mail_bot.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. import itertools
  4. import random
  5. from odoo import models, _
  6. class MailBot(models.AbstractModel):
  7. _name = 'mail.bot'
  8. _description = 'Mail Bot'
  9. def _apply_logic(self, record, values, command=None):
  10. """ Apply bot logic to generate an answer (or not) for the user
  11. The logic will only be applied if odoobot is in a chat with a user or
  12. if someone pinged odoobot.
  13. :param record: the mail_thread (or mail_channel) where the user
  14. message was posted/odoobot will answer.
  15. :param values: msg_values of the message_post or other values needed by logic
  16. :param command: the name of the called command if the logic is not triggered by a message_post
  17. """
  18. odoobot_id = self.env['ir.model.data']._xmlid_to_res_id("base.partner_root")
  19. if len(record) != 1 or values.get("author_id") == odoobot_id or values.get("message_type") != "comment" and not command:
  20. return
  21. if self._is_bot_pinged(values) or self._is_bot_in_private_channel(record):
  22. body = values.get("body", "").replace(u'\xa0', u' ').strip().lower().strip(".!")
  23. answer = self._get_answer(record, body, values, command)
  24. if answer:
  25. message_type = 'comment'
  26. subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment')
  27. record.with_context(mail_create_nosubscribe=True).sudo().message_post(body=answer, author_id=odoobot_id, message_type=message_type, subtype_id=subtype_id)
  28. def _get_answer(self, record, body, values, command=False):
  29. # onboarding
  30. odoobot_state = self.env.user.odoobot_state
  31. if self._is_bot_in_private_channel(record):
  32. # main flow
  33. if odoobot_state == 'onboarding_emoji' and self._body_contains_emoji(body):
  34. self.env.user.odoobot_state = "onboarding_command"
  35. self.env.user.odoobot_failed = False
  36. return _("Great! 👍<br/>To access special commands, <b>start your sentence with</b> <span class=\"o_odoobot_command\">/</span>. Try getting help.")
  37. elif odoobot_state == 'onboarding_command' and command == 'help':
  38. self.env.user.odoobot_state = "onboarding_ping"
  39. self.env.user.odoobot_failed = False
  40. return _("Wow you are a natural!<br/>Ping someone with @username to grab their attention. <b>Try to ping me using</b> <span class=\"o_odoobot_command\">@OdooBot</span> in a sentence.")
  41. elif odoobot_state == 'onboarding_ping' and self._is_bot_pinged(values):
  42. self.env.user.odoobot_state = "onboarding_attachement"
  43. self.env.user.odoobot_failed = False
  44. return _("Yep, I am here! 🎉 <br/>Now, try <b>sending an attachment</b>, like a picture of your cute dog...")
  45. elif odoobot_state == 'onboarding_attachement' and values.get("attachment_ids"):
  46. self.env.user.odoobot_state = "idle"
  47. self.env.user.odoobot_failed = False
  48. return _("I am a simple bot, but if that's a dog, he is the cutest 😊 <br/>Congratulations, you finished this tour. You can now <b>close this chat window</b>. Enjoy discovering Odoo.")
  49. elif odoobot_state in (False, "idle", "not_initialized") and (_('start the tour') in body.lower()):
  50. self.env.user.odoobot_state = "onboarding_emoji"
  51. return _("To start, try to send me an emoji :)")
  52. # easter eggs
  53. elif odoobot_state == "idle" and body in ['❤️', _('i love you'), _('love')]:
  54. 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 ❤️")
  55. elif _('fuck') in body or "fuck" in body:
  56. return _("That's not nice! I'm a bot but I have feelings... 💔")
  57. # help message
  58. elif self._is_help_requested(body) or odoobot_state == 'idle':
  59. return _("Unfortunately, I'm just a bot 😞 I don't understand! If you need help discovering our product, please check "
  60. "<a href=\"https://www.odoo.com/documentation\" target=\"_blank\">our documentation</a> or "
  61. "<a href=\"https://www.odoo.com/slides\" target=\"_blank\">our videos</a>.")
  62. else:
  63. # repeat question
  64. if odoobot_state == 'onboarding_emoji':
  65. self.env.user.odoobot_failed = True
  66. return _("Not exactly. To continue the tour, send an emoji: <b>type</b> <span class=\"o_odoobot_command\">:)</span> and press enter.")
  67. elif odoobot_state == 'onboarding_attachement':
  68. self.env.user.odoobot_failed = True
  69. return _("To <b>send an attachment</b>, click on the <i class=\"fa fa-paperclip\" aria-hidden=\"true\"></i> icon and select a file.")
  70. elif odoobot_state == 'onboarding_command':
  71. self.env.user.odoobot_failed = True
  72. return _("Not sure what you are doing. Please, type <span class=\"o_odoobot_command\">/</span> and wait for the propositions. Select <span class=\"o_odoobot_command\">help</span> and press enter")
  73. elif odoobot_state == 'onboarding_ping':
  74. self.env.user.odoobot_failed = True
  75. return _("Sorry, I am not listening. To get someone's attention, <b>ping him</b>. Write <span class=\"o_odoobot_command\">@OdooBot</span> and select me.")
  76. return random.choice([
  77. _("I'm not smart enough to answer your question.<br/>To follow my guide, ask: <span class=\"o_odoobot_command\">start the tour</span>."),
  78. _("Hmmm..."),
  79. _("I'm afraid I don't understand. Sorry!"),
  80. _("Sorry I'm sleepy. Or not! Maybe I'm just trying to hide my unawareness of human language...<br/>I can show you features if you write: <span class=\"o_odoobot_command\">start the tour</span>.")
  81. ])
  82. return False
  83. def _body_contains_emoji(self, body):
  84. # coming from https://unicode.org/emoji/charts/full-emoji-list.html
  85. emoji_list = itertools.chain(
  86. range(0x231A, 0x231c),
  87. range(0x23E9, 0x23f4),
  88. range(0x23F8, 0x23fb),
  89. range(0x25AA, 0x25ac),
  90. range(0x25FB, 0x25ff),
  91. range(0x2600, 0x2605),
  92. range(0x2614, 0x2616),
  93. range(0x2622, 0x2624),
  94. range(0x262E, 0x2630),
  95. range(0x2638, 0x263b),
  96. range(0x2648, 0x2654),
  97. range(0x265F, 0x2661),
  98. range(0x2665, 0x2667),
  99. range(0x267E, 0x2680),
  100. range(0x2692, 0x2698),
  101. range(0x269B, 0x269d),
  102. range(0x26A0, 0x26a2),
  103. range(0x26AA, 0x26ac),
  104. range(0x26B0, 0x26b2),
  105. range(0x26BD, 0x26bf),
  106. range(0x26C4, 0x26c6),
  107. range(0x26D3, 0x26d5),
  108. range(0x26E9, 0x26eb),
  109. range(0x26F0, 0x26f6),
  110. range(0x26F7, 0x26fb),
  111. range(0x2708, 0x270a),
  112. range(0x270A, 0x270c),
  113. range(0x270C, 0x270e),
  114. range(0x2733, 0x2735),
  115. range(0x2753, 0x2756),
  116. range(0x2763, 0x2765),
  117. range(0x2795, 0x2798),
  118. range(0x2934, 0x2936),
  119. range(0x2B05, 0x2b08),
  120. range(0x2B1B, 0x2b1d),
  121. range(0x1F170, 0x1f172),
  122. range(0x1F191, 0x1f19b),
  123. range(0x1F1E6, 0x1f200),
  124. range(0x1F201, 0x1f203),
  125. range(0x1F232, 0x1f23b),
  126. range(0x1F250, 0x1f252),
  127. range(0x1F300, 0x1f321),
  128. range(0x1F324, 0x1f32d),
  129. range(0x1F32D, 0x1f330),
  130. range(0x1F330, 0x1f336),
  131. range(0x1F337, 0x1f37d),
  132. range(0x1F37E, 0x1f380),
  133. range(0x1F380, 0x1f394),
  134. range(0x1F396, 0x1f398),
  135. range(0x1F399, 0x1f39c),
  136. range(0x1F39E, 0x1f3a0),
  137. range(0x1F3A0, 0x1f3c5),
  138. range(0x1F3C6, 0x1f3cb),
  139. range(0x1F3CB, 0x1f3cf),
  140. range(0x1F3CF, 0x1f3d4),
  141. range(0x1F3D4, 0x1f3e0),
  142. range(0x1F3E0, 0x1f3f1),
  143. range(0x1F3F3, 0x1f3f6),
  144. range(0x1F3F8, 0x1f400),
  145. range(0x1F400, 0x1f43f),
  146. range(0x1F442, 0x1f4f8),
  147. range(0x1F4F9, 0x1f4fd),
  148. range(0x1F500, 0x1f53e),
  149. range(0x1F549, 0x1f54b),
  150. range(0x1F54B, 0x1f54f),
  151. range(0x1F550, 0x1f568),
  152. range(0x1F56F, 0x1f571),
  153. range(0x1F573, 0x1f57a),
  154. range(0x1F58A, 0x1f58e),
  155. range(0x1F595, 0x1f597),
  156. range(0x1F5B1, 0x1f5b3),
  157. range(0x1F5C2, 0x1f5c5),
  158. range(0x1F5D1, 0x1f5d4),
  159. range(0x1F5DC, 0x1f5df),
  160. range(0x1F5FB, 0x1f600),
  161. range(0x1F601, 0x1f611),
  162. range(0x1F612, 0x1f615),
  163. range(0x1F61C, 0x1f61f),
  164. range(0x1F620, 0x1f626),
  165. range(0x1F626, 0x1f628),
  166. range(0x1F628, 0x1f62c),
  167. range(0x1F62E, 0x1f630),
  168. range(0x1F630, 0x1f634),
  169. range(0x1F635, 0x1f641),
  170. range(0x1F641, 0x1f643),
  171. range(0x1F643, 0x1f645),
  172. range(0x1F645, 0x1f650),
  173. range(0x1F680, 0x1f6c6),
  174. range(0x1F6CB, 0x1f6d0),
  175. range(0x1F6D1, 0x1f6d3),
  176. range(0x1F6E0, 0x1f6e6),
  177. range(0x1F6EB, 0x1f6ed),
  178. range(0x1F6F4, 0x1f6f7),
  179. range(0x1F6F7, 0x1f6f9),
  180. range(0x1F910, 0x1f919),
  181. range(0x1F919, 0x1f91f),
  182. range(0x1F920, 0x1f928),
  183. range(0x1F928, 0x1f930),
  184. range(0x1F931, 0x1f933),
  185. range(0x1F933, 0x1f93b),
  186. range(0x1F93C, 0x1f93f),
  187. range(0x1F940, 0x1f946),
  188. range(0x1F947, 0x1f94c),
  189. range(0x1F94D, 0x1f950),
  190. range(0x1F950, 0x1f95f),
  191. range(0x1F95F, 0x1f96c),
  192. range(0x1F96C, 0x1f971),
  193. range(0x1F973, 0x1f977),
  194. range(0x1F97C, 0x1f980),
  195. range(0x1F980, 0x1f985),
  196. range(0x1F985, 0x1f992),
  197. range(0x1F992, 0x1f998),
  198. range(0x1F998, 0x1f9a3),
  199. range(0x1F9B0, 0x1f9ba),
  200. range(0x1F9C1, 0x1f9c3),
  201. range(0x1F9D0, 0x1f9e7),
  202. range(0x1F9E7, 0x1fa00),
  203. [0x2328, 0x23cf, 0x24c2, 0x25b6, 0x25c0, 0x260e, 0x2611, 0x2618, 0x261d, 0x2620, 0x2626,
  204. 0x262a, 0x2640, 0x2642, 0x2663, 0x2668, 0x267b, 0x2699, 0x26c8, 0x26ce, 0x26cf,
  205. 0x26d1, 0x26fd, 0x2702, 0x2705, 0x270f, 0x2712, 0x2714, 0x2716, 0x271d, 0x2721, 0x2728, 0x2744, 0x2747, 0x274c,
  206. 0x274e, 0x2757, 0x27a1, 0x27b0, 0x27bf, 0x2b50, 0x2b55, 0x3030, 0x303d, 0x3297, 0x3299, 0x1f004, 0x1f0cf, 0x1f17e,
  207. 0x1f17f, 0x1f18e, 0x1f21a, 0x1f22f, 0x1f321, 0x1f336, 0x1f37d, 0x1f3c5, 0x1f3f7, 0x1f43f, 0x1f440, 0x1f441, 0x1f4f8,
  208. 0x1f4fd, 0x1f4ff, 0x1f57a, 0x1f587, 0x1f590, 0x1f5a4, 0x1f5a5, 0x1f5a8, 0x1f5bc, 0x1f5e1, 0x1f5e3, 0x1f5e8, 0x1f5ef,
  209. 0x1f5f3, 0x1f5fa, 0x1f600, 0x1f611, 0x1f615, 0x1f616, 0x1f617, 0x1f618, 0x1f619, 0x1f61a, 0x1f61b, 0x1f61f, 0x1f62c,
  210. 0x1f62d, 0x1f634, 0x1f6d0, 0x1f6e9, 0x1f6f0, 0x1f6f3, 0x1f6f9, 0x1f91f, 0x1f930, 0x1f94c, 0x1f97a, 0x1f9c0]
  211. )
  212. if any(chr(emoji) in body for emoji in emoji_list):
  213. return True
  214. return False
  215. def _is_bot_pinged(self, values):
  216. odoobot_id = self.env['ir.model.data']._xmlid_to_res_id("base.partner_root")
  217. return odoobot_id in values.get('partner_ids', [])
  218. def _is_bot_in_private_channel(self, record):
  219. odoobot_id = self.env['ir.model.data']._xmlid_to_res_id("base.partner_root")
  220. if record._name == 'mail.channel' and record.channel_type == 'chat':
  221. return odoobot_id in record.with_context(active_test=False).channel_partner_ids.ids
  222. return False
  223. def _is_help_requested(self, body):
  224. """Returns whether a message linking to the documentation and videos
  225. should be sent back to the user.
  226. """
  227. return any(token in body for token in ['help', _('help'), '?']) or self.env.user.odoobot_failed