[ADD] mail_broker

pull/1305/head
Olga Marco 2021-12-15 12:13:58 +01:00 committed by Enric Tobella
parent d301521db7
commit 1e5b8ad89c
27 changed files with 1927 additions and 0 deletions

View File

@ -0,0 +1,64 @@
===========
Mail Broker
===========
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-tegin%2Fcb--addons-lightgray.png?logo=github
:target: https://github.com/tegin/cb-addons/tree/13.0/mail_broker
:alt: tegin/cb-addons
|badge1| |badge2| |badge3|
This module allows to respond chats as a bot.
This way, a group of users can respond customers or any other set
of partners in an integrated way.
It is not intended to be integrated on default chatter as users don't need
to review again when one has responded.
**Table of contents**
.. contents::
:local:
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/tegin/cb-addons/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/tegin/cb-addons/issues/new?body=module:%20mail_broker%0Aversion:%2013.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Creu Blanca
Contributors
~~~~~~~~~~~~
* Enric Tobella <etobella@creublanca.es>
* Olga Marco <olga.marco@creublanca.es>
Maintainers
~~~~~~~~~~~
This module is part of the `tegin/cb-addons <https://github.com/tegin/cb-addons/tree/13.0/mail_broker>`_ project on GitHub.
You are welcome to contribute.

View File

@ -0,0 +1,3 @@
from . import controllers
from . import models
from .hooks import pre_init_hook

View File

@ -0,0 +1,22 @@
# Copyright 2020 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "Mail Broker",
"summary": """
Set a broker""",
"version": "13.0.1.0.0",
"license": "AGPL-3",
"author": "Creu Blanca,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/social",
"qweb": ["static/src/xml/broker.xml"],
"depends": ["mail"],
"pre_init_hook": "pre_init_hook",
"data": [
"security/security.xml",
"security/ir.model.access.csv",
"views/mail_broker.xml",
"templates/assets.xml",
"views/mail_broker_channel.xml",
],
}

View File

@ -0,0 +1 @@
from . import main

View File

@ -0,0 +1,22 @@
from odoo import http
from odoo.http import request
from odoo.addons.mail.controllers.bus import MailChatController
from odoo.addons.mail.controllers.main import MailController
class NewMailController(MailController):
@http.route("/mail/init_messaging", type="json", auth="user")
def mail_init_messaging(self):
result = super().mail_init_messaging()
result["broker_slots"] = request.env["mail.broker"].broker_fetch_slot()
return result
class NewMailChatController(MailChatController):
def _poll(self, dbname, channels, last, options):
if request.session.uid:
if request.env.user.has_group("mail_broker.broker_user"):
for bot in request.env["mail.broker"].search([]):
channels.append((request.db, "mail.broker", bot.id))
return super()._poll(dbname, channels, last, options)

View File

@ -0,0 +1,12 @@
def pre_init_hook(cr):
"""
The objective of this hook is to speed up the installation
of the module on an existing Odoo instance.
Without this script, big databases can take a long time to install this
module.
"""
cr.execute(
"""ALTER TABLE mail_message
ADD COLUMN broker_channel_id int"""
)

View File

@ -0,0 +1,4 @@
from . import mail_message
from . import mail_message_broker
from . import mail_broker_channel
from . import mail_broker

View File

@ -0,0 +1,48 @@
# Copyright 2020 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class MailBroker(models.Model):
_name = "mail.broker"
_description = "Mail Broker"
name = fields.Char(required=True)
token = fields.Char(required=True)
_sql_constraints = [
("mail_broker_token", "unique(token)", "Token must be unique"),
]
broker_type = fields.Selection([], required=True)
show_on_app = fields.Boolean(default=True)
webhook_url = fields.Char()
webhook_user_id = fields.Many2one(
"res.users", default=lambda self: self.env.user.id
)
@api.model
def broker_fetch_slot(self):
result = []
for record in self.search([("show_on_app", "=", True)]):
result.append(
{
"id": record.id,
"name": record.name,
"channel_name": "broker_%s" % record.id,
"threads": [
thread._get_thread_data()
for thread in self.env["mail.broker.channel"].search(
[("show_on_app", "=", True), ("broker_id", "=", record.id)]
)
],
}
)
return result
def channel_search(self, name):
self.ensure_one()
domain = [("broker_id", "=", self.id)]
if name:
domain += [("name", "ilike", "%" + name + "%")]
return self.env["mail.broker.channel"].search(domain).read(["name"])

View File

@ -0,0 +1,156 @@
# Copyright 2020 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from datetime import datetime
from xmlrpc.client import DateTime
from odoo import api, fields, models
class MailBrokerChannel(models.Model):
_name = "mail.broker.channel"
_description = "Mail Broker Channel"
name = fields.Char(required=True)
active = fields.Boolean(default=True)
token = fields.Char(required=True)
broker_id = fields.Many2one("mail.broker", required=True)
message_ids = fields.One2many("mail.message.broker", inverse_name="channel_id",)
mail_message_ids = fields.One2many(
"mail.message", inverse_name="broker_channel_id",
)
last_message_date = fields.Datetime(compute="_compute_message_data", store=True,)
unread = fields.Integer(compute="_compute_message_data", store=True,)
broker_token = fields.Char(related="broker_id.token", store=True, required=False)
show_on_app = fields.Boolean()
partner_id = fields.Many2one("res.partner")
message_main_attachment_id = fields.Many2one(
string="Main Attachment", comodel_name="ir.attachment", index=True, copy=False,
)
def message_fetch(self, domain=False, limit=30):
self.ensure_one()
if not domain:
domain = []
return (
self.env["mail.message"]
.search([("broker_channel_id", "=", self.id)] + domain, limit=limit)
.message_format()
)
@api.depends(
"mail_message_ids", "mail_message_ids.date", "mail_message_ids.broker_unread",
)
def _compute_message_data(self):
for r in self:
r.last_message_date = (
self.env["mail.message"]
.search([("broker_channel_id", "=", r.id)], limit=1, order="date DESC",)
.date
)
r.unread = self.env["mail.message"].search_count(
[("broker_channel_id", "=", r.id), ("broker_unread", "=", True)]
)
def _get_thread_data(self):
return {
"id": "broker_thread_%s" % self.id,
"res_id": self.id,
"name": self.name,
"last_message_date": self.last_message_date,
"channel_type": "broker_thread",
"unread": self.unread,
"broker_id": self.broker_id.id,
}
def _broker_message_post_vals(self, body, **kwargs):
subtype_id = kwargs.get("subtype_id", False)
if not subtype_id:
subtype = kwargs.get("subtype") or "mt_note"
if "." not in subtype:
subtype = "mail.%s" % subtype
subtype_id = self.env["ir.model.data"].xmlid_to_res_id(subtype)
vals = {
"channel_id": self.id,
"body": body,
"subtype_id": subtype_id,
"model": self._name,
"res_id": self.id,
}
if kwargs.get("author_id", False):
vals["author_id"] = kwargs["author_id"]
if "date" in kwargs:
date = kwargs["date"]
if isinstance(date, DateTime):
date = datetime.strptime(str(date), "%Y%m%dT%H:%M:%S")
vals["date"] = date
if "message_id" in kwargs:
vals["message_id"] = kwargs["message_id"]
vals["broker_unread"] = kwargs.get("broker_unread", False)
vals["attachment_ids"] = []
for attachment_id in kwargs.get("attachment_ids", []):
vals["attachment_ids"].append((4, attachment_id))
for name, content, mimetype in kwargs.get("attachments", []):
vals["attachment_ids"].append(
(
0,
0,
{
"name": name,
"datas": content.encode("utf-8"),
"type": "binary",
"description": name,
"mimetype": mimetype,
},
)
)
return vals
@api.returns("mail.message.broker", lambda value: value.id)
def message_post_broker(self, body=False, broker_type=False, **kwargs):
self.ensure_one()
if (
not body
and not kwargs.get("attachments")
and not kwargs.get("attachment_ids")
):
return False
vals = self._broker_message_post_vals(
body, broker_unread=True, author_id=self.partner_id.id, **kwargs
)
vals["state"] = "received"
vals["broker_type"] = broker_type
return self.env["mail.message.broker"].create(vals)
@api.model_create_multi
def create(self, vals_list):
channels = super().create(vals_list)
notifications = []
for channel in channels:
if channel.show_on_app and channel.broker_id.show_on_app:
notifications.append(
(
(self._cr.dbname, "mail.broker", channel.broker_id.id),
{"thread": channel._get_thread_data()},
)
)
if notifications:
self.env["bus.bus"].sendmany(notifications)
return channels
@api.returns("mail.message.broker", lambda value: value.id)
def broker_message_post(self, body=False, **kwargs):
self.ensure_one()
if not body and not kwargs.get("attachment_ids"):
return
message = (
self.with_context(do_not_notify=True)
.env["mail.message.broker"]
.create(self._broker_message_post_vals(body, **kwargs))
)
message.send()
self.env["bus.bus"].sendone(
(self._cr.dbname, "mail.broker", message.channel_id.broker_id.id),
{"message": message.mail_message_id.message_format()[0]},
)
return message

View File

@ -0,0 +1,50 @@
# Copyright 2020 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class MailMessage(models.Model):
_inherit = "mail.message"
broker_channel_id = fields.Many2one(
"mail.broker.channel",
readonly=True,
compute="_compute_broker_channel_id",
store=True,
)
broker_unread = fields.Boolean(default=False)
broker_type = fields.Selection([("telegram", "Telegram")], required=True)
broker_notification_ids = fields.One2many(
"mail.message.broker", inverse_name="mail_message_id"
)
@api.depends("broker_notification_ids")
def _compute_broker_channel_id(self):
for rec in self:
if rec.broker_notification_ids:
rec.broker_channel_id = rec.broker_notification_ids[0].channel_id
@api.model
def _message_read_dict_postprocess(self, messages, message_tree):
result = super()._message_read_dict_postprocess(messages, message_tree)
for message_dict in messages:
message_id = message_dict.get("id")
message = message_tree[message_id]
notifications = message.broker_notification_ids
if notifications:
message_dict.update(
{
"broker_channel_id": message.broker_channel_id.id,
"broker_unread": message.broker_unread,
"customer_status": "received"
if all(d.state == "received" for d in notifications)
else message_dict.get("customer_status", False),
}
)
return result
def set_message_done(self):
self.write({"broker_unread": False})
return super().set_message_done()

View File

@ -0,0 +1,82 @@
# Copyright 2020 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from odoo import api, fields, models
_logger = logging.getLogger(__name__)
class MailMessageBroker(models.Model):
_name = "mail.message.broker"
_description = "Broker Message"
_inherits = {"mail.message": "mail_message_id"}
_order = "id desc"
_rec_name = "subject"
# content
mail_message_id = fields.Many2one(
"mail.message",
"Mail Message",
required=True,
ondelete="cascade",
index=True,
auto_join=True,
)
message_id = fields.Char(readonly=True)
channel_id = fields.Many2one(
"mail.broker.channel", required=True, ondelete="cascade"
)
state = fields.Selection(
[
("outgoing", "Outgoing"),
("sent", "Sent"),
("exception", "Delivery Failed"),
("cancel", "Cancelled"),
("received", "Received"),
],
"Status",
readonly=True,
copy=False,
default="outgoing",
)
failure_reason = fields.Text(
"Failure Reason",
readonly=1,
help="Failure reason. This is usually the exception thrown by the"
" email server, stored to ease the debugging of mailing issues.",
)
@api.model_create_multi
def create(self, vals_list):
messages = super().create(vals_list)
if self.env.context.get("notify_broker", True):
notifications = []
for message in messages:
notifications.append(
[
(
self._cr.dbname,
"mail.broker",
message.channel_id.broker_id.id,
),
{"message": message.mail_message_id.message_format()[0]},
]
)
self.env["bus.bus"].sendmany(notifications)
return messages
def send(self, auto_commit=False, raise_exception=False, parse_mode="HTML"):
for record in self:
getattr(record, "_send_%s" % record.channel_id.broker_id.broker_type)(
auto_commit=auto_commit,
raise_exception=raise_exception,
parse_mode=parse_mode,
)
def mark_outgoing(self):
return self.write({"state": "outgoing"})
def cancel(self):
return self.write({"state": "cancel"})

View File

@ -0,0 +1,2 @@
* Enric Tobella <etobella@creublanca.es>
* Olga Marco <olga.marco@creublanca.es>

View File

@ -0,0 +1,7 @@
This module allows to respond chats as a bot.
This way, a group of users can respond customers or any other set
of partners in an integrated way.
It is not intended to be integrated on default chatter as users don't need
to review again when one has responded.

View File

@ -0,0 +1,10 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_mail_message_broker_all,mail.message.broker.all,model_mail_message_broker,,1,0,0,0
access_mail_message_broker_portal,mail.message.broker.portal,model_mail_message_broker,base.group_portal,1,1,1,0
access_mail_message_broker_user,mail.message.broker.user,model_mail_message_broker,base.group_user,1,1,1,0
access_mail_message_broker_system,mail.message.broker.system,model_mail_message_broker,base.group_system,1,1,1,1
access_mail_broker_channel_all,mail.telegram.chat.all,model_mail_broker_channel,,1,0,0,0
access_mail_broker_channel_system,mail_broker_channel,model_mail_broker_channel,base.group_system,1,1,1,1
access_mail_broker_all,mail.telegram.bot.all,model_mail_broker,,1,0,0,0
access_mail_broker_channel_user,mail_broker_manager_bot,model_mail_broker_channel,mail_broker.broker_user,1,1,0,0
access_mail_broker_system,mail_broker,model_mail_broker,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_mail_message_broker_all mail.message.broker.all model_mail_message_broker 1 0 0 0
3 access_mail_message_broker_portal mail.message.broker.portal model_mail_message_broker base.group_portal 1 1 1 0
4 access_mail_message_broker_user mail.message.broker.user model_mail_message_broker base.group_user 1 1 1 0
5 access_mail_message_broker_system mail.message.broker.system model_mail_message_broker base.group_system 1 1 1 1
6 access_mail_broker_channel_all mail.telegram.chat.all model_mail_broker_channel 1 0 0 0
7 access_mail_broker_channel_system mail_broker_channel model_mail_broker_channel base.group_system 1 1 1 1
8 access_mail_broker_all mail.telegram.bot.all model_mail_broker 1 0 0 0
9 access_mail_broker_channel_user mail_broker_manager_bot model_mail_broker_channel mail_broker.broker_user 1 1 0 0
10 access_mail_broker_system mail_broker model_mail_broker base.group_system 1 1 1 1

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record model="ir.module.category" id="module_category_broker">
<field name="name">Broker</field>
</record>
<record model="res.groups" id="broker_user">
<field name="name">User</field>
<field name="category_id" ref="module_category_broker" />
<field
name="users"
eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"
/>
</record>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -0,0 +1,160 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="Capa_1"
data-name="Capa 1"
viewBox="0 0 200.33 200"
version="1.1"
sodipodi:docname="icon.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
inkscape:export-filename="/home/operador/pyworkspace12/social/mail_broker/static/description/icon.png"
inkscape:export-xdpi="95.841858"
inkscape:export-ydpi="95.841858">
<metadata
id="metadata24">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>icon</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1853"
inkscape:window-height="1025"
id="namedview22"
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:snap-text-baseline="true"
inkscape:zoom="1.668772"
inkscape:cx="32.080636"
inkscape:cy="54.692004"
inkscape:window-x="67"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="layer2">
<sodipodi:guide
position="51.271186,126.37712"
orientation="-0.70710678,0.70710678"
id="guide40"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="118.65012,168.08767"
orientation="-0.70710678,0.70710678"
id="guide42"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="138.02966,143.00847"
orientation="-0.70710678,0.70710678"
id="guide46"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="137.92373,118.22034"
orientation="-0.70710678,0.70710678"
id="guide48"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="156.99152,93.008474"
orientation="-0.70710678,0.70710678"
id="guide50"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="139.19491,46.822034"
orientation="-0.70710678,0.70710678"
id="guide52"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="33.070725,104.38065"
orientation="-0.70710678,0.70710678"
id="guide839"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="136.62741,46.516241"
orientation="-0.70710678,0.70710678"
id="guide841"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
</sodipodi:namedview>
<defs
id="defs4">
<style
id="style2">.cls-1{fill:none;}.cls-2{fill:#3b588f;}.cls-3{fill:#070308;opacity:0.4;}.cls-4{fill:#fff;}</style>
</defs>
<title
id="title6">icon</title>
<rect
id="_Sector_"
data-name="&lt;Sector&gt;"
class="cls-1"
width="200"
height="200" />
<rect
class="cls-2"
x="0.33000001"
width="200"
height="200"
id="rect9"
ry="5.6928086"
y="0"
style="fill:#179cde;fill-opacity:1" />
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="Layer 1"
style="display:inline" />
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 3">
<path
style="fill:#000000;fill-opacity:0.29051986;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 34.149962,94.540113 0.33939645,128.35068 c 0,22.04621 -0.005125,-37.02851 -0.005125,66.40594 0,1.19849 0.87634879,2.80824 1.47559179,3.40749 0,0 1.6732367,1.97005 4.5570938,1.85769 l 83.7317707,-0.009 47.844652,-47.84466 4.88661,-95.023805 z"
id="path843"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
</g>
<g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Layer 2"
style="display:inline">
<path
id="path828"
d="m 158.96325,57.709612 -19.05983,89.885658 c -1.43794,6.34387 -5.18788,7.92279 -10.51673,4.93412 l -29.04085,-21.4 -14.012923,13.47721 c -1.550725,1.55072 -2.84769,2.84769 -5.836362,2.84769 l 2.086431,-29.57656 53.824254,-48.636368 c 2.34019,-2.08643 -0.50751,-3.24242 -3.63715,-1.15599 L 66.229882,109.98314 37.583764,101.01713 c -6.23109,-1.945458 -6.34388,-6.231088 1.29697,-9.219758 L 150.92767,48.63082 c 5.18788,-1.945453 9.72728,1.155997 8.03558,9.078792 z"
inkscape:connector-curvature="0"
style="display:inline;fill:#ffffff;fill-opacity:1;stroke-width:0.28195" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -0,0 +1,419 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: http://docutils.sourceforge.net/" />
<title>Mail Broker</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="mail-broker">
<h1 class="title">Mail Broker</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/tegin/cb-addons/tree/13.0/mail_broker"><img alt="tegin/cb-addons" src="https://img.shields.io/badge/github-tegin%2Fcb--addons-lightgray.png?logo=github" /></a></p>
<p>This module allows to respond chats as a bot.</p>
<p>This way, a group of users can respond customers or any other set
of partners in an integrated way.</p>
<p>It is not intended to be integrated on default chatter as users dont need
to review again when one has responded.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#bug-tracker" id="id1">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id2">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id3">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id4">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id5">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id1">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/tegin/cb-addons/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/tegin/cb-addons/issues/new?body=module:%20mail_broker%0Aversion:%2013.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#id2">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id3">Authors</a></h2>
<ul class="simple">
<li>Creu Blanca</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id4">Contributors</a></h2>
<ul class="simple">
<li>Enric Tobella &lt;<a class="reference external" href="mailto:etobella&#64;creublanca.es">etobella&#64;creublanca.es</a>&gt;</li>
<li>Olga Marco &lt;<a class="reference external" href="mailto:olga.marco&#64;creublanca.es">olga.marco&#64;creublanca.es</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id5">Maintainers</a></h2>
<p>This module is part of the <a class="reference external" href="https://github.com/tegin/cb-addons/tree/13.0/mail_broker">tegin/cb-addons</a> project on GitHub.</p>
<p>You are welcome to contribute.</p>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,431 @@
odoo.define("mail_broker.Broker", function(require) {
"use strict";
var BasicComposer = require("mail.composer.Basic");
var ExtendedComposer = require("mail.composer.Extended");
var core = require("web.core");
var AbstractAction = require("web.AbstractAction");
// Var ControlPanelMixin = require("web.ControlPanelMixin");
var ThreadWidget = require("mail.widget.Thread");
var dom = require("web.dom");
var QWeb = core.qweb;
var _t = core._t;
var Broker = AbstractAction.extend({
contentTemplate: "mail_broker.broker",
events: {
"click .o_mail_channel_settings": "_onChannelSettingsClicked",
"click .o_mail_discuss_item": "_onSelectBrokerChannel",
"click .o_mail_sidebar_title .o_add": "_onSearchThread",
"blur .o_mail_add_thread input": "_onSearchThreadBlur",
},
init: function(parent, action, options) {
this._super.apply(this, arguments);
this.action = action;
this.action_manager = parent;
this.domain = [];
this.options = options || {};
this._threadsScrolltop = {};
this._composerStates = {};
this._defaultChatID =
this.options.active_id ||
this.action.context.active_id ||
this.action.params.default_active_id;
this._selectedMessage = null;
},
/**
* @override
*/
on_attach_callback: function() {
if (this._thread) {
this._threadWidget.scrollToPosition(
this._threadsScrolltop[this._thread.getID()]
);
this._loadEnoughMessages();
}
},
/**
* @override
*/
on_detach_callback: function() {
if (this._thread) {
this._threadsScrolltop[
this._thread.getID()
] = this._threadWidget.getScrolltop();
}
},
start: function() {
var self = this;
return this._super.apply(this, arguments).then(function() {
return self._initRender();
});
/*
Return this.alive($.when.apply($, defs))
.then(function() {
if (self._defaultChatID) {
return self.alive(self._setThread(self._defaultChatID));
}
})
.then(function() {
self._updateThreads();
self._startListening();
self._threadWidget.$el.on(
"scroll",
null,
_.debounce(function() {
var $noContent = self._threadWidget.$(".o_mail_no_content");
if (
self._threadWidget.getScrolltop() < 20 &&
!self._thread.isAllHistoryLoaded() &&
!$noContent.length
) {
self._loadMoreMessages();
}
if (self._threadWidget.isAtBottom()) {
self._thread.markAsRead();
}
}, 100)
);
});
*/
},
_initRender: function() {
var self = this;
this._basicComposer = new BasicComposer(this, {
mentionPartnersRestricted: true,
});
this._extendedComposer = new ExtendedComposer(this, {
mentionPartnersRestricted: true,
});
this._basicComposer
.on("post_message", this, this._onPostMessage)
.on("input_focused", this, this._onComposerFocused);
this._extendedComposer
.on("post_message", this, this._onPostMessage)
.on("input_focused", this, this._onComposerFocused);
this._renderButtons();
var defs = [];
defs.push(this._renderThread());
defs.push(this._basicComposer.appendTo(this.$(".o_mail_discuss_content")));
return Promise.all(defs)
.then(function() {
if (self._defaultChatID) {
return self._setThread(self._defaultChatID);
}
})
.then(function() {
self._updateThreads();
self._startListening();
self._threadWidget.$el.on(
"scroll",
null,
_.debounce(function() {
var $noContent = self._threadWidget.$(".o_mail_no_content");
if (
self._threadWidget.getScrolltop() < 20 &&
!self._thread.isAllHistoryLoaded() &&
!$noContent.length
) {
self._loadMoreMessages();
}
if (self._threadWidget.isAtBottom()) {
self._thread.markAsRead();
}
}, 100)
);
});
},
_startListening: function() {
this.call("mail_service", "getMailBus").on(
"new_message",
this,
this._onNewMessage
);
},
_setThread: function(threadID) {
this._storeThreadState();
var thread = this.call("mail_service", "getThread", threadID);
if (!thread) {
return;
}
this._thread = thread;
var self = this;
this.messagesSeparatorPosition = undefined;
return this._fetchAndRenderThread().then(function() {
self._thread.markAsRead();
// Restore scroll position and composer of the new
// current thread
self._restoreThreadState();
// Update control panel before focusing the composer, otherwise
// focus is on the searchview
self.set("title", self._thread.getTitle());
self.action_manager.do_push_state({
action: self.action.id,
active_id: self._thread.getID(),
});
});
},
_storeThreadState: function() {
if (this._thread) {
this._threadsScrolltop[
this._thread.getID()
] = this._threadWidget.getScrolltop();
}
},
_loadEnoughMessages: function() {
var $el = this._threadWidget.el;
var loadMoreMessages =
$el.clientHeight &&
$el.clientHeight === $el.scrollHeight &&
!this._thread.isAllHistoryLoaded();
if (loadMoreMessages) {
return this._loadMoreMessages().then(
this._loadEnoughMessages.bind(this)
);
}
},
_getThreadRenderingOptions: function() {
if (_.isUndefined(this.messagesSeparatorPosition)) {
if (this._unreadCounter) {
var messageID = this._thread.getLastSeenMessageID();
this.messagesSeparatorPosition = messageID || "top";
} else {
// No unread message -> don't display separator
this.messagesSeparatorPosition = false;
}
}
return {
displayLoadMore: !this._thread.isAllHistoryLoaded(),
squashCloseMessages: true,
messagesSeparatorPosition: this.messagesSeparatorPosition,
displayEmailIcons: false,
displayReplyIcons: false,
displayBottomThreadFreeSpace: true,
displayModerationCommands: false,
displayMarkAsRead: false,
displayDocumentLinks: false,
displayStars: false,
};
},
_fetchAndRenderThread: function() {
var self = this;
return this._thread.fetchMessages().then(function() {
self._threadWidget.render(
self._thread,
self._getThreadRenderingOptions()
);
return self._loadEnoughMessages();
});
},
_renderButtons: function() {
// This is a hook just in case some buttons are required
},
_renderThread: function() {
this._threadWidget = new ThreadWidget(this, {
areMessageAttachmentsDeletable: false,
loadMoreOnScroll: true,
});
this._threadWidget.on("load_more_messages", this, this._loadMoreMessages);
return this._threadWidget.appendTo(this.$(".o_mail_discuss_content"));
},
_renderSidebar: function(options) {
var $sidebar = $(
QWeb.render("mail_broker.broker.Sidebar", {
activeThreadID: this._thread ? this._thread.getID() : undefined,
bots: options.bots,
})
);
return $sidebar;
},
_restoreThreadState: function() {
var $newMessagesSeparator = this.$(".o_thread_new_messages_separator");
if ($newMessagesSeparator.length) {
this._threadWidget.$el.scrollTo($newMessagesSeparator);
} else {
var newThreadScrolltop = this._threadsScrolltop[this._thread.getID()];
this._threadWidget.scrollToPosition(newThreadScrolltop);
}
},
_updateThreads: function() {
var bots = this.call("mail_service", "getBrokerBots");
var $sidebar = this._renderSidebar({
bots: bots,
});
this.$(".o_mail_discuss_sidebar").html($sidebar.contents());
var self = this;
_.each(bots, function(bot, broker_id) {
var $input = self.$(
".o_mail_add_thread[data-bot=" + broker_id + "] input"
);
self._prepareAddThreadInput($input, broker_id, bot);
});
},
_prepareAddThreadInput: function($input, broker_id) {
var self = this;
$input.autocomplete({
source: function(request, response) {
self._lastSearchVal = _.escape(request.term);
self._searchChannel(broker_id, self._lastSearchVal).then(function(
result
) {
response(result);
});
},
select: function(ev, ui) {
self._setThread("broker_thread_" + ui.item.id);
self._updateThreads();
},
focus: function(ev) {
ev.preventDefault();
},
html: true,
});
},
_loadMoreMessages: function() {
var self = this;
var oldestMessageID = this.$(".o_thread_message")
.first()
.data("messageId");
var oldestMessageSelector =
'.o_thread_message[data-message-id="' + oldestMessageID + '"]';
var offset = -dom.getPosition(document.querySelector(oldestMessageSelector))
.top;
return this._thread.fetchMessages({loadMore: true}).then(function() {
if (self.messagesSeparatorPosition === "top") {
// Reset value to re-compute separator position
self.messagesSeparatorPosition = undefined;
}
self._threadWidget.render(
self._thread,
self._getThreadRenderingOptions()
);
offset += dom.getPosition(document.querySelector(oldestMessageSelector))
.top;
self._threadWidget.scrollToPosition(offset);
});
},
_onSearchThread: function(ev) {
ev.preventDefault();
var bot = $(ev.target).data("bot");
this.$(".o_mail_add_thread[data-bot=" + bot + "]")
.show()
.find("input")
.focus();
},
_onSearchThreadBlur: function() {
this.$(".o_mail_add_thread").hide();
},
_onChannelSettingsClicked: function(ev) {
ev.preventDefault();
ev.stopPropagation();
var threadID = $(ev.target).data("thread-id");
var thread = this.call("mail_service", "getThread", threadID);
this.do_action({
type: "ir.actions.act_window",
res_model: "mail.broker.channel",
res_id: thread.resId,
name: _t("Configure chat"),
views: [[false, "form"]],
target: "new",
});
},
_onNewMessage: function(message) {
var thread_id = "broker_thread_" + message.broker_channel_id;
if (this._thread && thread_id === this._thread.getID()) {
this._thread.markAsRead();
var shouldScroll = this._threadWidget.isAtBottom();
var self = this;
this._fetchAndRenderThread().then(function() {
if (shouldScroll) {
self._threadWidget.scrollToMessage({
msgID: message.getID(),
});
}
});
}
// Re-render sidebar to indicate that there is a new message in
// The corresponding threads
this._updateThreads();
// Dump scroll position of threads in which the new message arrived
this._threadsScrolltop = _.omit(
this._threadsScrolltop,
message.getThreadIDs()
);
},
_searchChannel: function(broker_id, searchVal) {
return this._rpc({
model: "mail.broker",
method: "channel_search",
args: [[parseInt(broker_id, 10)], searchVal],
}).then(function(result) {
var values = [];
_.each(result, function(channel) {
var escapedName = _.escape(channel.name);
values.push(
_.extend(channel, {
value: escapedName,
label: escapedName,
})
);
});
return values;
});
},
_onComposerFocused: function() {
// Hook
},
_onSelectBrokerChannel: function(ev) {
ev.preventDefault();
var threadID = $(ev.currentTarget).data("thread-id");
this._setThread(threadID);
this._updateThreads();
},
_onPostMessage: function(messageData) {
var self = this;
var options = {};
if (this._selectedMessage) {
messageData.subtype = this._selectedMessage.isNote()
? "mail.mt_note"
: "mail.mt_comment";
messageData.subtype_id = false;
messageData.broker_type = "comment";
options.documentID = this._selectedMessage.getDocumentID();
options.documentModel = this._selectedMessage.getDocumentModel();
}
this._thread
.postMessage(messageData, options)
.then(function() {
if (self._selectedMessage) {
self._renderSnackbar(
"mail.discuss.MessageSentSnackbar",
{
documentName: self._selectedMessage.getDocumentName(),
},
5000
);
self._unselectMessage();
} else {
self._threadWidget.scrollToBottom();
}
})
.catch(function() {
// TODO: Display notifications
});
},
});
core.action_registry.add("mail.broker", Broker);
return Broker;
});

View File

@ -0,0 +1,85 @@
odoo.define("mail_broker.mail.Manager", function(require) {
"use strict";
var Manager = require("mail.Manager");
var BrokerThread = require("mail_broker.BrokerThread");
Manager.include({
_addMessageToThreads: function(message, options) {
this._super.apply(this, arguments);
if (message.broker_channel_id) {
var thread = this.getThread(
"broker_thread_" + message.broker_channel_id
);
if (thread) {
thread.addMessage(message, options);
}
}
},
_updateInternalStateFromServer: function(result) {
this._super.apply(this, arguments);
this._updateBrokerChannelFromServer(result);
},
getBrokerBots: function() {
var data = _.extend({}, this._broker_bots);
_.each(data, function(value) {
value.threads = [];
});
_.each(this._threads, function(thread) {
if (thread.getType() === "broker_thread") {
data[thread.broker_id].threads.push(thread);
}
});
_.each(data, function(value) {
value.threads.sort(function(a, b) {
return b.last_message_date - a.last_message_date;
});
});
return data;
},
_updateBrokerChannelFromServer: function(data) {
var self = this;
this._broker_bots = {};
_.each(data.broker_slots, function(slot) {
self._broker_bots[slot.id] = {
name: slot.name,
channel_name: slot.channel_name,
};
_.each(slot.threads, self._addChannel.bind(self));
});
},
getMailBrokerThreads: function() {
var data = _.filter(this._threads, function(thread) {
return thread.getType() === "broker_thread";
});
data = data.sort(function(a, b) {
return b.last_message_date - a.last_message_date;
});
return data;
},
_makeChannel: function(data, options) {
if (data.channel_type === "broker_thread") {
return new BrokerThread({
parent: this,
data: data,
options: options,
commands: this._commands,
});
}
return this._super.apply(this, arguments);
},
_onNotification: function(notifs) {
var self = this;
var result = this._super.apply(this, arguments);
_.each(notifs, function(notif) {
if (notif[0][1] === "mail.broker") {
if (notif[1].message) {
self.addMessage(notif[1].message, {silent: 0});
} else if (notif[1].thread) {
self._addChannel(notif[1].thread);
}
}
});
return result;
},
});
});

View File

@ -0,0 +1,16 @@
odoo.define("mail_broker.model.Message", function(require) {
"use strict";
var Message = require("mail.model.Message");
Message.include({
init: function(parent, data) {
this._super.apply(this, arguments);
this.broker_channel_id = data.broker_channel_id || false;
this.broker_unread = data.broker_unread || false;
},
isNeedaction: function() {
return this._super.apply(this, arguments) || this.broker_unread;
},
});
});

View File

@ -0,0 +1,118 @@
odoo.define("mail_broker.BrokerThread", function(require) {
"use strict";
var Thread = require("mail.model.Thread");
var session = require("web.session");
var field_utils = require("web.field_utils");
var BrokerThread = Thread.extend({
init: function(params) {
this._messageIDs = [];
var data = params.data;
data.type = "broker_thread";
this.resId = data.res_id;
this._super.apply(this, arguments);
this._messages = [];
this.last_message_date = field_utils.parse.datetime(data.last_message_date);
this.allHistoryLoaded = false;
this.broker_id = data.broker_id;
this._unreadCounter = data.unread;
},
getMessages: function() {
return this._messages;
},
getLastSeenMessageID: function() {
return null;
},
getNeedactionCounter: function() {
return this._unreadCounter;
},
isGroupBasedSubscription: function() {
return true;
},
_addMessage: function(message) {
this._super.apply(this, arguments);
if (_.contains(this._messages, message)) {
return;
}
// Update internal list of messages
this._messages.push(message);
this._messages = _.sortBy(this._messages, function(msg) {
return msg.getID();
});
// Update message ids associated to this document thread
if (!_.contains(this._messageIDs, message.getID())) {
this._messageIDs.push(message.getID());
}
if (message._date > this.last_message_date) {
this.last_message_date = message._date;
}
if (message.isNeedaction()) {
this._unreadCounter++;
}
},
isAllHistoryLoaded: function() {
return this.allHistoryLoaded;
},
fetchMessages: function(options) {
return this._fetchMessages(options);
},
_fetchMessages: function(options) {
var self = this;
var domain = [];
if (options && options.loadMore) {
var minMessageID = this._messages[0].getID();
domain = [["id", "<", minMessageID]].concat(domain);
}
return this._rpc({
model: "mail.broker.channel",
method: "message_fetch",
args: [[this.resId], domain],
kwargs: this._getFetchMessagesKwargs(options),
}).then(function(messages) {
if (!self.allHistoryLoaded) {
self.allHistoryLoaded = messages.length < self._FETCH_LIMIT;
}
_.each(messages, function(messageData) {
self.call("mail_service", "addMessage", messageData, {
silent: true,
});
});
});
},
_getFetchMessagesKwargs: function() {
return {
limit: this._FETCH_LIMIT,
context: session.user_context,
};
},
_postMessage: function(data) {
var self = this;
return this._super.apply(this, arguments).then(function(messageData) {
_.extend(messageData, {
broker_type: "comment",
subtype: "mail.mt_comment",
command: data.command,
});
return self
._rpc({
model: "mail.broker.channel",
method: "broker_message_post",
args: [[self.resId]],
kwargs: messageData,
})
.then(function() {
return messageData;
});
});
},
_markAsRead: function() {
var self = this;
return this._super.apply(this, arguments).then(function() {
self.call("mail_service", "markMessagesAsRead", self._messageIDs);
});
},
});
return BrokerThread;
});

View File

@ -0,0 +1,3 @@
.o_thread_message_broker_received {
color: green;
}

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="mail_broker.broker">
<div class="o_mail_discuss">
<div class="o_mail_discuss_sidebar" />
<div class="o_mail_discuss_content">
</div>
</div>
</t>
<t t-name="mail_broker.broker.Sidebar">
<div class="o_mail_discuss_sidebar">
<t t-foreach="bots" t-as="bot_key">
<t t-set="type">broker_thread</t>
<t t-set="bot" t-value="bots[bot_key]" />
<t t-set="channels" t-value="bot.threads" />
<t t-call="mail.broker.discuss.SidebarTitle">
<t t-set="title" t-value="bot.name" />
<t t-set="icon" t-value="fa-users" />
</t>
<t t-call="mail.broker.discuss.SidebarItems">
<t t-set="displayHash" t-value="false" />
<t t-set="inputPlaceholder">Search a chat</t>
</t>
</t>
</div>
</t>
<t t-name="mail.broker.discuss.SidebarTitle" t-extend="mail.discuss.SidebarTitle">
<t t-jquery=".o_add" t-operation="attributes">
<attribute name="t-attf-data-bot">#{bot_key}</attribute>
</t>
</t>
<t t-name="mail.broker.discuss.SidebarItems" t-extend="mail.discuss.SidebarItems">
<t t-jquery=".o_mail_add_thread" t-operation="attributes">
<attribute name="t-attf-data-bot">#{bot_key}</attribute>
</t>
<t t-jquery=".o_input" t-operation="attributes">
<attribute name="t-attf-data-bot">#{bot_key}</attribute>
</t>
</t>
</templates>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<template id="assets_backend" name="Broker assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script
type="text/javascript"
src="/mail_broker/static/src/js/message.js"
/>
<script
type="text/javascript"
src="/mail_broker/static/src/js/manager.js"
/>
<script type="text/javascript" src="/mail_broker/static/src/js/thread.js" />
<script type="text/javascript" src="/mail_broker/static/src/js/broker.js" />
<link rel="stylesheet" href="/mail_broker/static/src/scss/broker.scss" />
</xpath>
</template>
</odoo>

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Creu Blanca
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.ui.view" id="mail_broker_form_view">
<field name="name">mail.broker.form</field>
<field name="model">mail.broker</field>
<field name="arch" type="xml">
<form>
<header />
<sheet>
<div class="oe_button_box" name="button_box" />
<group>
<field name="name" />
<field name="show_on_app" />
<field name="token" />
<field name="broker_type" />
<field name="webhook_url" />
<field name="webhook_user_id" />
</group>
</sheet>
<div class="oe_chatter" />
</form>
</field>
</record>
<record model="ir.ui.view" id="mail_broker_search_view">
<field name="name">mail.broker.search</field>
<field name="model">mail.broker</field>
<field name="arch" type="xml">
<search>
<field name="name" />
</search>
</field>
</record>
<record model="ir.ui.view" id="mail_broker_tree_view">
<field name="name">mail.broker.tree</field>
<field name="model">mail.broker</field>
<field name="arch" type="xml">
<tree>
<field name="name" />
<field name="token" />
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="mail_broker_act_window">
<field name="name">Mail Broker</field>
<field name="res_model">mail.broker</field>
<field name="view_mode">tree,form</field>
<field name="domain">[]</field>
<field name="context">{}</field>
</record>
<record model="ir.ui.menu" id="mail_broker_menu">
<field name="name">Mail Broker</field>
<field name="parent_id" ref="base.menu_email" />
<field name="action" ref="mail_broker_act_window" />
<field name="sequence" eval="16" />
</record>
</odoo>

View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Creu Blanca
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.ui.view" id="mail_broker_channel_form_view">
<field name="name">mail.broker.channel.form</field>
<field name="model">mail.broker.channel</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<form>
<header />
<sheet>
<div class="oe_button_box" name="button_box" />
<group>
<field name="name" />
<field name="partner_id" />
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="mail_broker_channel_form_root_view">
<field name="name">mail.broker.channel.form</field>
<field name="model">mail.broker.channel</field>
<field name="inherit_id" ref="mail_broker.mail_broker_channel_form_view" />
<field name="groups_id" eval="[(6, 0, [ref('base.group_system')])]" />
<field name="arch" type="xml">
<field name="name" position="after">
<field name="token" />
<field name="broker_id" />
<field name="show_on_app" />
<field name="token" />
</field>
<xpath expr="//div[@name='button_box']" position="inside" />
</field>
</record>
<record model="ir.ui.view" id="mail_broker_channel_search_view">
<field name="name">mail.broker.channel.search (in mail_broker)</field>
<field name="model">mail.broker.channel</field>
<field name="arch" type="xml">
<search>
<field name="name" />
</search>
</field>
</record>
<record model="ir.ui.view" id="mail_broker_channel_tree_view">
<field name="name">mail.broker.channel.tree (in mail_broker)</field>
<field name="model">mail.broker.channel</field>
<field name="arch" type="xml">
<tree>
<field name="name" />
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="mail_broker_channel_act_window">
<field name="name">Mail Broken Channel</field>
<field name="res_model">mail.broker.channel</field>
<field name="view_mode">tree,form</field>
<field name="domain">[]</field>
<field name="context">{}</field>
</record>
<record model="ir.ui.menu" id="mail_broker_channel_menu">
<field name="name">Mail Broken Channel</field>
<field name="parent_id" ref="base.menu_email" />
<field name="action" ref="mail_broker_channel_act_window" />
<field name="sequence" eval="16" />
</record>
<record id="mail_broker_action_window" model="ir.actions.client">
<field name="name">Broker</field>
<field name="tag">mail.broker</field>
<field name="res_model">mail.broker.channel</field>
<field name="params" eval="{}" />
</record>
<menuitem
name="Broker"
id="mail_broker_channel"
sequence="2"
web_icon="mail_broker,static/description/icon.png"
action="mail_broker_action_window"
groups="mail_broker.broker_user"
/>
</odoo>