mirror of
https://github.com/bringout/oca-ocb-mail.git
synced 2026-04-22 15:02:01 +02:00
19.0 vanilla
This commit is contained in:
parent
5df8c07b59
commit
daa394e8b0
2114 changed files with 564841 additions and 299642 deletions
|
|
@ -1,5 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import attachment
|
||||
from . import channel
|
||||
from . import chatbot
|
||||
from . import main
|
||||
from . import rtc
|
||||
from . import thread
|
||||
from . import webclient
|
||||
from . import cors
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from odoo import _
|
||||
from odoo.http import route, request
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.addons.mail.controllers.attachment import AttachmentController
|
||||
from odoo.addons.mail.tools.discuss import add_guest_to_context
|
||||
|
||||
|
||||
class LivechatAttachmentController(AttachmentController):
|
||||
@route()
|
||||
@add_guest_to_context
|
||||
def mail_attachment_upload(self, ufile, thread_id, thread_model, is_pending=False, **kwargs):
|
||||
thread = self._get_thread_with_access_for_post(thread_model, thread_id, **kwargs)
|
||||
if not thread:
|
||||
raise NotFound()
|
||||
if (
|
||||
thread_model == "discuss.channel"
|
||||
and thread.channel_type == "livechat"
|
||||
and thread.livechat_end_dt
|
||||
and not request.env.user._is_internal()
|
||||
):
|
||||
raise AccessError(_("You are not allowed to upload attachments on this channel."))
|
||||
return super().mail_attachment_upload(ufile, thread_id, thread_model, is_pending, **kwargs)
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from markupsafe import Markup
|
||||
from werkzeug.exceptions import BadRequest, NotFound
|
||||
|
||||
from odoo import Command
|
||||
from odoo.http import request, route
|
||||
from odoo.addons.mail.controllers.discuss.channel import ChannelController
|
||||
|
||||
|
||||
class LivechatChannelController(ChannelController):
|
||||
@route("/im_livechat/session/update_note", auth="user", methods=["POST"], type="jsonrpc")
|
||||
def livechat_session_update_note(self, channel_id, note):
|
||||
"""Internal users having the rights to read the session can update its note."""
|
||||
if self.env.user.share:
|
||||
raise NotFound()
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel:
|
||||
raise NotFound()
|
||||
# sudo: discuss.channel - internal users having the rights to read the session can update its note
|
||||
# Markup: note sanitized when written on the field
|
||||
channel.sudo().livechat_note = Markup(note)
|
||||
|
||||
@route("/im_livechat/session/update_status", auth="user", methods=["POST"], type="jsonrpc")
|
||||
def livechat_session_update_status(self, channel_id, livechat_status):
|
||||
"""Internal users having the rights to read the session can update its status."""
|
||||
if self.env.user.share:
|
||||
raise NotFound()
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel:
|
||||
raise NotFound()
|
||||
# sudo: discuss.channel - internal users having the rights to read the session can update its status
|
||||
channel.sudo().livechat_status = livechat_status
|
||||
|
||||
@route("/im_livechat/conversation/update_tags", auth="user", methods=["POST"], type="jsonrpc")
|
||||
def livechat_conversation_update_tags(self, channel_id, tag_ids, method="ADD"):
|
||||
"""Add or remove tags from a live chat conversation."""
|
||||
if not self.env["im_livechat.conversation.tag"].has_access("write"):
|
||||
raise NotFound()
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel:
|
||||
raise NotFound()
|
||||
# sudo: discuss.channel - internal users having the rights to read the conversation and to
|
||||
# write tags can update the tags
|
||||
if method == "ADD":
|
||||
channel.sudo().livechat_conversation_tag_ids = [
|
||||
Command.link(tag_id) for tag_id in tag_ids
|
||||
]
|
||||
elif method == "DELETE":
|
||||
channel.sudo().livechat_conversation_tag_ids = [
|
||||
Command.unlink(tag_id) for tag_id in tag_ids
|
||||
]
|
||||
if channel.livechat_status == "need_help":
|
||||
request.env.ref("im_livechat.im_livechat_group_user")._bus_send(
|
||||
"im_livechat.looking_for_help/tags",
|
||||
{
|
||||
"channel_id": channel.id,
|
||||
"tag_ids": channel.sudo().livechat_conversation_tag_ids.ids,
|
||||
},
|
||||
subchannel="LOOKING_FOR_HELP",
|
||||
)
|
||||
|
||||
@route(
|
||||
"/im_livechat/conversation/write_expertises", auth="user", methods=["POST"], type="jsonrpc"
|
||||
)
|
||||
def livechat_conversation_write_expertises(self, channel_id, orm_commands):
|
||||
if any(cmd[0] not in (Command.LINK, Command.UNLINK) for cmd in orm_commands):
|
||||
raise BadRequest(
|
||||
self.env._("Write expertises: Only LINK and UNLINK commands are allowed.")
|
||||
)
|
||||
if not self.env.user.has_group("im_livechat.im_livechat_group_user"):
|
||||
return
|
||||
if channel := request.env["discuss.channel"].search(
|
||||
[("id", "=", channel_id), ("channel_type", "=", "livechat")]
|
||||
):
|
||||
# sudo: discuss.channel - live chat users can update the expertises of any live chat.
|
||||
channel.sudo().livechat_expertise_ids = orm_commands
|
||||
|
||||
@route(
|
||||
"/im_livechat/conversation/create_and_link_expertise",
|
||||
auth="user",
|
||||
methods=["POST"],
|
||||
type="jsonrpc",
|
||||
)
|
||||
def livechat_conversation_create_and_link_expertise(self, channel_id, expertise_name):
|
||||
channel = request.env["discuss.channel"].search(
|
||||
[("id", "=", channel_id), ("channel_type", "=", "livechat")]
|
||||
)
|
||||
if not channel:
|
||||
return
|
||||
stripped_name = expertise_name.strip()
|
||||
expertise = request.env["im_livechat.expertise"].search([("name", "=", stripped_name)])
|
||||
if not expertise:
|
||||
expertise = request.env["im_livechat.expertise"].create({"name": stripped_name})
|
||||
channel.livechat_expertise_ids = [Command.link(expertise.id)]
|
||||
|
|
@ -1,109 +1,145 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import http
|
||||
from odoo import fields, http
|
||||
from odoo.http import request
|
||||
from odoo.tools import get_lang, is_html_empty, plaintext2html
|
||||
from odoo.addons.mail.tools.discuss import add_guest_to_context, Store
|
||||
|
||||
|
||||
class LivechatChatbotScriptController(http.Controller):
|
||||
@http.route('/chatbot/restart', type="json", auth="public", cors="*")
|
||||
def chatbot_restart(self, channel_uuid, chatbot_script_id):
|
||||
chatbot_language = self._get_chatbot_language()
|
||||
mail_channel = request.env['mail.channel'].sudo().with_context(lang=chatbot_language).search([('uuid', '=', channel_uuid)], limit=1)
|
||||
@http.route("/chatbot/restart", type="jsonrpc", auth="public")
|
||||
@add_guest_to_context
|
||||
def chatbot_restart(self, channel_id, chatbot_script_id):
|
||||
discuss_channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
chatbot = request.env['chatbot.script'].browse(chatbot_script_id)
|
||||
if not mail_channel or not chatbot.exists():
|
||||
if not discuss_channel or not chatbot.exists():
|
||||
return None
|
||||
chatbot_language = chatbot._get_chatbot_language()
|
||||
message = discuss_channel.with_context(lang=chatbot_language)._chatbot_restart(chatbot)
|
||||
return {
|
||||
"message_id": message.id,
|
||||
"store_data": Store().add(message).get_result(),
|
||||
}
|
||||
|
||||
return mail_channel._chatbot_restart(chatbot).message_format()[0]
|
||||
|
||||
@http.route('/chatbot/post_welcome_steps', type="json", auth="public", cors="*")
|
||||
def chatbot_post_welcome_steps(self, channel_uuid, chatbot_script_id):
|
||||
mail_channel = request.env['mail.channel'].sudo().search([('uuid', '=', channel_uuid)], limit=1)
|
||||
chatbot_language = self._get_chatbot_language()
|
||||
chatbot = request.env['chatbot.script'].sudo().with_context(lang=chatbot_language).browse(chatbot_script_id)
|
||||
if not mail_channel or not chatbot.exists():
|
||||
return None
|
||||
|
||||
return chatbot._post_welcome_steps(mail_channel).message_format()
|
||||
|
||||
@http.route('/chatbot/answer/save', type="json", auth="public", cors="*")
|
||||
def chatbot_save_answer(self, channel_uuid, message_id, selected_answer_id):
|
||||
mail_channel = request.env['mail.channel'].sudo().search([('uuid', '=', channel_uuid)], limit=1)
|
||||
@http.route("/chatbot/answer/save", type="jsonrpc", auth="public")
|
||||
@add_guest_to_context
|
||||
def chatbot_save_answer(self, channel_id, message_id, selected_answer_id):
|
||||
discuss_channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
chatbot_message = request.env['chatbot.message'].sudo().search([
|
||||
('mail_message_id', '=', message_id),
|
||||
('mail_channel_id', '=', mail_channel.id),
|
||||
('discuss_channel_id', '=', discuss_channel.id),
|
||||
], limit=1)
|
||||
selected_answer = request.env['chatbot.script.answer'].sudo().browse(selected_answer_id)
|
||||
|
||||
if not mail_channel or not chatbot_message or not selected_answer.exists():
|
||||
if not discuss_channel or not chatbot_message or not selected_answer.exists():
|
||||
return
|
||||
|
||||
if selected_answer in chatbot_message.script_step_id.answer_ids:
|
||||
chatbot_message.write({'user_script_answer_id': selected_answer_id})
|
||||
|
||||
@http.route('/chatbot/step/trigger', type="json", auth="public", cors="*")
|
||||
def chatbot_trigger_step(self, channel_uuid, chatbot_script_id=None):
|
||||
chatbot_language = self._get_chatbot_language()
|
||||
mail_channel = request.env['mail.channel'].sudo().with_context(lang=chatbot_language).search([('uuid', '=', channel_uuid)], limit=1)
|
||||
if not mail_channel:
|
||||
@http.route("/chatbot/step/trigger", type="jsonrpc", auth="public")
|
||||
@add_guest_to_context
|
||||
def chatbot_trigger_step(self, channel_id, chatbot_script_id=None, data_id=None):
|
||||
chatbot_language = self.env["chatbot.script"]._get_chatbot_language()
|
||||
discuss_channel = request.env["discuss.channel"].with_context(lang=chatbot_language).search([("id", "=", channel_id)])
|
||||
if not discuss_channel:
|
||||
return None
|
||||
|
||||
next_step = False
|
||||
if mail_channel.chatbot_current_step_id:
|
||||
chatbot = mail_channel.chatbot_current_step_id.chatbot_script_id
|
||||
user_messages = mail_channel.message_ids.filtered(
|
||||
lambda message: message.author_id != chatbot.operator_partner_id
|
||||
)
|
||||
user_answer = request.env['mail.message'].sudo()
|
||||
if user_messages:
|
||||
user_answer = user_messages.sorted(lambda message: message.id)[-1]
|
||||
next_step = mail_channel.chatbot_current_step_id._process_answer(mail_channel, user_answer.body)
|
||||
# sudo: chatbot.script.step - visitor can access current step of the script
|
||||
if current_step := discuss_channel.sudo().chatbot_current_step_id:
|
||||
if (
|
||||
current_step.is_forward_operator
|
||||
and discuss_channel.livechat_operator_id
|
||||
!= current_step.chatbot_script_id.operator_partner_id
|
||||
):
|
||||
return None
|
||||
chatbot = current_step.chatbot_script_id
|
||||
domain = [
|
||||
("author_id", "!=", chatbot.operator_partner_id.id),
|
||||
("model", "=", "discuss.channel"),
|
||||
("res_id", "=", channel_id),
|
||||
]
|
||||
# sudo: mail.message - accessing last message to process answer is allowed
|
||||
user_answer = self.env["mail.message"].sudo().search(domain, order="id desc", limit=1)
|
||||
next_step = current_step._process_answer(discuss_channel, user_answer.body)
|
||||
elif chatbot_script_id: # when restarting, we don't have a "current step" -> set "next" as first step of the script
|
||||
chatbot = request.env['chatbot.script'].sudo().with_context(lang=chatbot_language).browse(chatbot_script_id)
|
||||
chatbot = request.env['chatbot.script'].sudo().browse(chatbot_script_id).with_context(lang=chatbot_language)
|
||||
if chatbot.exists():
|
||||
next_step = chatbot.script_step_ids[:1]
|
||||
|
||||
partner, guest = self.env["res.partner"]._get_current_persona()
|
||||
store = Store(bus_channel=partner or guest)
|
||||
store.data_id = data_id
|
||||
if not next_step:
|
||||
# sudo - discuss.channel: marking the channel as closed as part of the chat bot flow
|
||||
discuss_channel.sudo().livechat_end_dt = fields.Datetime.now()
|
||||
store.resolve_data_request()
|
||||
store.bus_send()
|
||||
return None
|
||||
|
||||
posted_message = next_step._process_step(mail_channel)
|
||||
return {
|
||||
'chatbot_posted_message': posted_message.message_format()[0] if posted_message else None,
|
||||
'chatbot_step': {
|
||||
'chatbot_operator_found': next_step.step_type == 'forward_operator' and len(
|
||||
mail_channel.channel_member_ids) > 2,
|
||||
'chatbot_script_step_id': next_step.id,
|
||||
'chatbot_step_answers': [{
|
||||
'id': answer.id,
|
||||
'label': answer.name,
|
||||
'redirect_link': answer.redirect_link,
|
||||
} for answer in next_step.answer_ids],
|
||||
'chatbot_step_is_last': next_step._is_last_step(mail_channel),
|
||||
'chatbot_step_message': plaintext2html(next_step.message) if not is_html_empty(next_step.message) else False,
|
||||
'chatbot_step_type': next_step.step_type,
|
||||
}
|
||||
}
|
||||
|
||||
@http.route('/chatbot/step/validate_email', type="json", auth="public", cors="*")
|
||||
def chatbot_validate_email(self, channel_uuid):
|
||||
mail_channel = request.env['mail.channel'].sudo().search([('uuid', '=', channel_uuid)], limit=1)
|
||||
if not mail_channel or not mail_channel.chatbot_current_step_id:
|
||||
return None
|
||||
|
||||
chatbot = mail_channel.chatbot_current_step_id.chatbot_script_id
|
||||
user_messages = mail_channel.message_ids.filtered(
|
||||
lambda message: message.author_id != chatbot.operator_partner_id
|
||||
# sudo: discuss.channel - updating current step on the channel is allowed
|
||||
discuss_channel.sudo().chatbot_current_step_id = next_step.id
|
||||
posted_message = next_step._process_step(discuss_channel)
|
||||
store.add(posted_message).add(next_step)
|
||||
store.resolve_data_request(
|
||||
chatbot_step={"scriptStep": next_step.id, "message": posted_message.id}
|
||||
)
|
||||
chatbot_next_step_id = (next_step.id, posted_message.id)
|
||||
store.add_model_values(
|
||||
"ChatbotStep",
|
||||
{
|
||||
"id": chatbot_next_step_id,
|
||||
"isLast": next_step._is_last_step(discuss_channel),
|
||||
"message": posted_message.id,
|
||||
"operatorFound": next_step.is_forward_operator
|
||||
and discuss_channel.livechat_operator_id != chatbot.operator_partner_id,
|
||||
"scriptStep": next_step.id,
|
||||
},
|
||||
)
|
||||
store.add_model_values(
|
||||
"Chatbot",
|
||||
{
|
||||
"currentStep": {
|
||||
"id": chatbot_next_step_id,
|
||||
"scriptStep": next_step.id,
|
||||
"message": posted_message.id,
|
||||
},
|
||||
"id": (chatbot.id, discuss_channel.id),
|
||||
"script": chatbot.id,
|
||||
"thread": Store.One(discuss_channel, [], as_thread=True),
|
||||
"steps": [("ADD", [{
|
||||
"scriptStep": chatbot_next_step_id[0],
|
||||
"message": chatbot_next_step_id[1]
|
||||
}])],
|
||||
},
|
||||
)
|
||||
store.bus_send()
|
||||
|
||||
if user_messages:
|
||||
user_answer = user_messages.sorted(lambda message: message.id)[-1]
|
||||
result = chatbot._validate_email(user_answer.body, mail_channel)
|
||||
|
||||
if result['posted_message']:
|
||||
result['posted_message'] = result['posted_message'].message_format()[0]
|
||||
@http.route("/chatbot/step/validate_email", type="jsonrpc", auth="public")
|
||||
@add_guest_to_context
|
||||
def chatbot_validate_email(self, channel_id):
|
||||
discuss_channel = (
|
||||
request.env["discuss.channel"]
|
||||
.search([("id", "=", channel_id)])
|
||||
.with_context(lang=self.env["chatbot.script"]._get_chatbot_language())
|
||||
)
|
||||
if not discuss_channel or not discuss_channel.chatbot_current_step_id:
|
||||
return None
|
||||
|
||||
# sudo: chatbot.script - visitor can access chatbot script of their channel
|
||||
chatbot = discuss_channel.sudo().chatbot_current_step_id.chatbot_script_id
|
||||
domain = [
|
||||
("author_id", "!=", chatbot.operator_partner_id.id),
|
||||
("model", "=", "discuss.channel"),
|
||||
("res_id", "=", channel_id),
|
||||
]
|
||||
# sudo: mail.message - accessing last message to validate email is allowed
|
||||
last_user_message = self.env["mail.message"].sudo().search(domain, order="id desc", limit=1)
|
||||
result = {}
|
||||
if last_user_message:
|
||||
result = chatbot._validate_email(last_user_message.body, discuss_channel)
|
||||
if posted_message := result.pop("posted_message"):
|
||||
store = Store().add(posted_message)
|
||||
store.add(discuss_channel, {
|
||||
"messages": Store.Many(posted_message, mode="ADD")
|
||||
})
|
||||
result["data"] = store.get_result()
|
||||
return result
|
||||
|
||||
def _get_chatbot_language(self):
|
||||
return request.httprequest.cookies.get('frontend_lang', request.env.user.lang or get_lang(request.env).code)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import attachment
|
||||
from . import chatbot
|
||||
from . import channel
|
||||
from . import link_preview
|
||||
from . import main
|
||||
from . import message_reaction
|
||||
from . import rtc
|
||||
from . import thread
|
||||
from . import webclient
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
from odoo.http import route
|
||||
from odoo.addons.im_livechat.tools.misc import force_guest_env
|
||||
from odoo.addons.mail.controllers.attachment import AttachmentController
|
||||
|
||||
|
||||
class LivechatAttachmentController(AttachmentController):
|
||||
@route("/im_livechat/cors/attachment/upload", auth="public", cors="*", csrf=False)
|
||||
def im_livechat_attachment_upload(self, guest_token, ufile, thread_id, thread_model, is_pending=False, **kwargs):
|
||||
force_guest_env(guest_token)
|
||||
return self.mail_attachment_upload(ufile, thread_id, thread_model, is_pending, **kwargs)
|
||||
|
||||
@route("/im_livechat/cors/attachment/delete", methods=["POST"], type="jsonrpc", auth="public", cors="*")
|
||||
def im_livechat_attachment_delete(self, guest_token, attachment_id, access_token=None):
|
||||
force_guest_env(guest_token)
|
||||
return self.mail_attachment_delete(attachment_id, access_token)
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.http import route
|
||||
from odoo.addons.mail.controllers.discuss.channel import ChannelController
|
||||
from odoo.addons.im_livechat.tools.misc import force_guest_env
|
||||
|
||||
|
||||
class LivechatChannelController(ChannelController):
|
||||
@route("/im_livechat/cors/channel/messages", methods=["POST"], type="jsonrpc", auth="public", cors="*")
|
||||
def livechat_channel_messages(self, guest_token, channel_id, fetch_params=None):
|
||||
force_guest_env(guest_token)
|
||||
return self.discuss_channel_messages(channel_id, fetch_params)
|
||||
|
||||
@route("/im_livechat/cors/channel/mark_as_read", methods=["POST"], type="jsonrpc", auth="public", cors="*")
|
||||
def livechat_channel_mark_as_read(self, guest_token, **kwargs):
|
||||
force_guest_env(guest_token)
|
||||
return self.discuss_channel_mark_as_read(**kwargs)
|
||||
|
||||
@route("/im_livechat/cors/channel/notify_typing", methods=["POST"], type="jsonrpc", auth="public", cors="*")
|
||||
def livechat_channel_notify_typing(self, guest_token, channel_id, is_typing):
|
||||
force_guest_env(guest_token)
|
||||
return self.discuss_channel_notify_typing(channel_id, is_typing)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.http import route
|
||||
from odoo.addons.im_livechat.controllers.chatbot import LivechatChatbotScriptController
|
||||
from odoo.addons.im_livechat.tools.misc import force_guest_env
|
||||
|
||||
|
||||
class CorsLivechatChatbotScriptController(LivechatChatbotScriptController):
|
||||
@route("/chatbot/cors/restart", type="jsonrpc", auth="public", cors="*")
|
||||
def cors_chatbot_restart(self, guest_token, channel_id, chatbot_script_id):
|
||||
force_guest_env(guest_token)
|
||||
return self.chatbot_restart(channel_id, chatbot_script_id)
|
||||
|
||||
@route("/chatbot/cors/answer/save", type="jsonrpc", auth="public", cors="*")
|
||||
def cors_chatbot_save_answer(self, guest_token, channel_id, message_id, selected_answer_id):
|
||||
force_guest_env(guest_token)
|
||||
return self.chatbot_save_answer(channel_id, message_id, selected_answer_id)
|
||||
|
||||
@route("/chatbot/cors/step/trigger", type="jsonrpc", auth="public", cors="*")
|
||||
def cors_chatbot_trigger_step(self, guest_token, channel_id, chatbot_script_id=None, data_id=None):
|
||||
force_guest_env(guest_token)
|
||||
return self.chatbot_trigger_step(channel_id, chatbot_script_id, data_id)
|
||||
|
||||
@route("/chatbot/cors/step/validate_email", type="jsonrpc", auth="public", cors="*")
|
||||
def cors_chatbot_validate_email(self, guest_token, channel_id):
|
||||
force_guest_env(guest_token)
|
||||
return self.chatbot_validate_email(channel_id)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.http import route
|
||||
from odoo.addons.mail.controllers.link_preview import LinkPreviewController
|
||||
from odoo.addons.im_livechat.tools.misc import force_guest_env
|
||||
|
||||
|
||||
class LivechatLinkPreviewController(LinkPreviewController):
|
||||
@route("/im_livechat/cors/link_preview", methods=["POST"], type="jsonrpc", auth="public", cors="*")
|
||||
def livechat_link_preview(self, guest_token, message_id):
|
||||
force_guest_env(guest_token)
|
||||
self.mail_link_preview(message_id)
|
||||
|
||||
@route("/im_livechat/cors/link_preview/hide", methods=["POST"], type="jsonrpc", auth="public", cors="*")
|
||||
def livechat_link_preview_hide(self, guest_token, message_link_preview_ids):
|
||||
force_guest_env(guest_token)
|
||||
self.mail_link_preview_hide(message_link_preview_ids)
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.http import route
|
||||
from odoo.addons.im_livechat.controllers.main import LivechatController
|
||||
from odoo.addons.im_livechat.tools.misc import force_guest_env
|
||||
|
||||
|
||||
class CorsLivechatController(LivechatController):
|
||||
@route("/im_livechat/cors/visitor_leave_session", type="jsonrpc", auth="public", cors="*")
|
||||
def cors_visitor_leave_session(self, guest_token, channel_id):
|
||||
force_guest_env(guest_token)
|
||||
self.visitor_leave_session(channel_id)
|
||||
|
||||
@route("/im_livechat/cors/feedback", type="jsonrpc", auth="public", cors="*")
|
||||
def cors_feedback(self, guest_token, channel_id, rate, reason=None):
|
||||
force_guest_env(guest_token)
|
||||
self.feedback(channel_id, rate, reason)
|
||||
|
||||
@route("/im_livechat/cors/history", type="jsonrpc", auth="public", cors="*")
|
||||
def cors_history_pages(self, guest_token, pid, channel_id, page_history=None):
|
||||
force_guest_env(guest_token)
|
||||
return self.history_pages(pid, channel_id, page_history)
|
||||
|
||||
@route("/im_livechat/cors/download_transcript/<int:channel_id>", type="http", auth="public", cors="*")
|
||||
def cors_download_livechat_transcript(self, guest_token, channel_id):
|
||||
force_guest_env(guest_token)
|
||||
return self.download_livechat_transcript(channel_id)
|
||||
|
||||
@route("/im_livechat/cors/get_session", methods=["POST"], type="jsonrpc", auth="public", cors="*")
|
||||
def cors_get_session(
|
||||
self, channel_id, previous_operator_id=None, chatbot_script_id=None, persisted=True, **kwargs
|
||||
):
|
||||
force_guest_env(kwargs.pop("guest_token", ""), raise_if_not_found=False)
|
||||
return self.get_session(
|
||||
channel_id, previous_operator_id, chatbot_script_id, persisted, **kwargs
|
||||
)
|
||||
|
||||
@route("/im_livechat/cors/init", type="jsonrpc", auth="public", cors="*")
|
||||
def cors_livechat_init(self, channel_id, guest_token=""):
|
||||
force_guest_env(guest_token, raise_if_not_found=False)
|
||||
return self.livechat_init(channel_id)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.http import route
|
||||
from odoo.addons.mail.controllers.message_reaction import MessageReactionController
|
||||
from odoo.addons.im_livechat.tools.misc import force_guest_env
|
||||
|
||||
|
||||
class LivechatMessageReactionController(MessageReactionController):
|
||||
@route("/im_livechat/cors/message/reaction", methods=["POST"], type="jsonrpc", auth="public", cors="*")
|
||||
def livechat_message_reaction(self, guest_token, message_id, content, action, **kwargs):
|
||||
force_guest_env(guest_token)
|
||||
return self.mail_message_reaction(message_id, content, action, **kwargs)
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.http import route
|
||||
from odoo.addons.mail.controllers.discuss.rtc import RtcController
|
||||
from odoo.addons.im_livechat.tools.misc import force_guest_env
|
||||
|
||||
|
||||
class LivechatRtcController(RtcController):
|
||||
@route("/im_livechat/cors/rtc/channel/join_call", methods=["POST"], type="jsonrpc", auth="public", cors="*")
|
||||
def livechat_channel_call_join(self, guest_token, channel_id, check_rtc_session_ids=None):
|
||||
force_guest_env(guest_token)
|
||||
return self.channel_call_join(channel_id, check_rtc_session_ids)
|
||||
|
||||
@route("/im_livechat/cors/rtc/channel/leave_call", methods=["POST"], type="jsonrpc", auth="public", cors="*")
|
||||
def livechat_channel_call_leave(self, guest_token, channel_id):
|
||||
force_guest_env(guest_token)
|
||||
return self.channel_call_leave(channel_id)
|
||||
|
||||
@route("/im_livechat/cors/rtc/session/update_and_broadcast", methods=["POST"], type="jsonrpc", auth="public", cors="*")
|
||||
def livechat_session_update_and_broadcast(self, guest_token, session_id, values):
|
||||
force_guest_env(guest_token)
|
||||
self.session_update_and_broadcast(session_id, values)
|
||||
|
||||
@route("/im_livechat/cors/rtc/session/notify_call_members", methods=["POST"], type="jsonrpc", auth="public", cors="*")
|
||||
def livechat_session_call_notify(self, guest_token, peer_notifications):
|
||||
force_guest_env(guest_token)
|
||||
self.session_call_notify(peer_notifications)
|
||||
|
||||
@route("/im_livechat/cors/channel/ping", methods=["POST"], type="jsonrpc", auth="public", cors="*")
|
||||
def livechat_channel_ping(self, guest_token, channel_id, rtc_session_id=None, check_rtc_session_ids=None):
|
||||
force_guest_env(guest_token)
|
||||
return self.channel_ping(channel_id, rtc_session_id, check_rtc_session_ids)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.http import route
|
||||
from odoo.addons.mail.controllers.thread import ThreadController
|
||||
from odoo.addons.im_livechat.tools.misc import force_guest_env
|
||||
|
||||
|
||||
class LivechatThreadController(ThreadController):
|
||||
@route("/im_livechat/cors/message/post", methods=["POST"], type="jsonrpc", auth="public", cors="*")
|
||||
def livechat_message_post(self, guest_token, thread_model, thread_id, post_data, context=None, **kwargs):
|
||||
force_guest_env(guest_token)
|
||||
return self.mail_message_post(thread_model, thread_id, post_data, context, **kwargs)
|
||||
|
||||
@route("/im_livechat/cors/message/update_content", methods=["POST"], type="jsonrpc", auth="public", cors="*")
|
||||
def livechat_message_update_content(self, guest_token, message_id, update_data, **kwargs):
|
||||
force_guest_env(guest_token)
|
||||
return self.mail_message_update_content(message_id, update_data, **kwargs)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.http import route
|
||||
from odoo.addons.mail.controllers.webclient import WebclientController
|
||||
from odoo.addons.im_livechat.tools.misc import force_guest_env
|
||||
|
||||
|
||||
class WebClient(WebclientController):
|
||||
"""Override to add CORS support."""
|
||||
|
||||
@route("/im_livechat/cors/action", methods=["POST"], type="jsonrpc", auth="public", cors="*")
|
||||
def livechat_action(self, guest_token="", **kwargs):
|
||||
force_guest_env(guest_token, raise_if_not_found=False)
|
||||
return self.mail_action(**kwargs)
|
||||
|
||||
@route("/im_livechat/cors/data", methods=["POST"], type="jsonrpc", auth="public", cors="*", readonly=True)
|
||||
def livechat_data(self, guest_token="", **kwargs):
|
||||
force_guest_env(guest_token, raise_if_not_found=False)
|
||||
return self.mail_data(**kwargs)
|
||||
|
|
@ -1,10 +1,14 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from markupsafe import Markup
|
||||
from werkzeug.exceptions import NotFound
|
||||
from urllib.parse import urlsplit
|
||||
from pytz import timezone
|
||||
|
||||
from odoo import http, tools, _
|
||||
from odoo.http import request
|
||||
from odoo.addons.base.models.assetsbundle import AssetsBundle
|
||||
from odoo import http, _
|
||||
from odoo.http import content_disposition, request
|
||||
from odoo.addons.base.models.ir_qweb_fields import nl2br
|
||||
from odoo.addons.mail.tools.discuss import add_guest_to_context, Store
|
||||
|
||||
|
||||
class LivechatController(http.Controller):
|
||||
|
|
@ -12,32 +16,49 @@ class LivechatController(http.Controller):
|
|||
# Note: the `cors` attribute on many routes is meant to allow the livechat
|
||||
# to be embedded in an external website.
|
||||
|
||||
@http.route('/im_livechat/external_lib.<any(css,js):ext>', type='http', auth='public')
|
||||
def livechat_lib(self, ext, **kwargs):
|
||||
# _get_asset return the bundle html code (script and link list) but we want to use the attachment content
|
||||
bundle = 'im_livechat.external_lib'
|
||||
files, _ = request.env["ir.qweb"]._get_asset_content(bundle)
|
||||
asset = AssetsBundle(bundle, files)
|
||||
@http.route('/im_livechat/external_lib.<any(css,js):ext>', type='http', auth='public', cors='*')
|
||||
def external_lib(self, ext, **kwargs):
|
||||
""" Preserve compatibility with legacy livechat imports. Only
|
||||
serves javascript since the css will be fetched by the shadow
|
||||
DOM of the livechat to avoid conflicts.
|
||||
"""
|
||||
if ext == 'css':
|
||||
raise request.not_found()
|
||||
return self.assets_embed(ext, **kwargs)
|
||||
|
||||
mock_attachment = getattr(asset, ext)()
|
||||
if isinstance(mock_attachment, list): # suppose that CSS asset will not required to be split in pages
|
||||
mock_attachment = mock_attachment[0]
|
||||
def _is_cors_request(self):
|
||||
headers = request.httprequest.headers
|
||||
origin_url = urlsplit(headers.get("referer"))
|
||||
return (
|
||||
origin_url.netloc != headers.get("host")
|
||||
or origin_url.scheme != request.httprequest.scheme
|
||||
)
|
||||
|
||||
stream = request.env['ir.binary']._get_stream_from(mock_attachment)
|
||||
@http.route('/im_livechat/assets_embed.<any(css, js):ext>', type='http', auth='public', cors='*')
|
||||
def assets_embed(self, ext, **kwargs):
|
||||
# If the request comes from a different origin, we must provide the CORS
|
||||
# assets to enable the redirection of routes to the CORS controller.
|
||||
bundle = "im_livechat.assets_embed_cors" if self._is_cors_request() else "im_livechat.assets_embed_external"
|
||||
asset = request.env["ir.qweb"]._get_asset_bundle(bundle)
|
||||
if ext not in ('css', 'js'):
|
||||
raise request.not_found()
|
||||
stream = request.env['ir.binary']._get_stream_from(getattr(asset, ext)())
|
||||
return stream.get_response()
|
||||
|
||||
@http.route('/im_livechat/load_templates', type='json', auth='none', cors="*")
|
||||
def load_templates(self, **kwargs):
|
||||
templates = self._livechat_templates_get()
|
||||
return [tools.file_open(tmpl, 'rb').read() for tmpl in templates]
|
||||
@http.route('/im_livechat/font-awesome', type='http', auth='none', cors="*")
|
||||
def fontawesome(self, **kwargs):
|
||||
return http.Stream.from_path('web/static/src/libs/fontawesome/fonts/fontawesome-webfont.woff2').get_response()
|
||||
|
||||
def _livechat_templates_get(self):
|
||||
return [
|
||||
'im_livechat/static/src/legacy/widgets/feedback/feedback.xml',
|
||||
'im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.xml',
|
||||
'im_livechat/static/src/legacy/widgets/public_livechat_view/public_livechat_view.xml',
|
||||
'im_livechat/static/src/legacy/public_livechat_chatbot.xml',
|
||||
]
|
||||
@http.route('/im_livechat/odoo_ui_icons', type='http', auth='none', cors="*")
|
||||
def odoo_ui_icons(self, **kwargs):
|
||||
return http.Stream.from_path('web/static/lib/odoo_ui_icons/fonts/odoo_ui_icons.woff2').get_response()
|
||||
|
||||
@http.route('/im_livechat/emoji_bundle', type='http', auth='public', cors='*')
|
||||
def get_emoji_bundle(self):
|
||||
bundle = 'web.assets_emoji'
|
||||
asset = request.env["ir.qweb"]._get_asset_bundle(bundle)
|
||||
stream = request.env['ir.binary']._get_stream_from(asset.js())
|
||||
return stream.get_response()
|
||||
|
||||
@http.route('/im_livechat/support/<int:channel_id>', type='http', auth='public')
|
||||
def support_page(self, channel_id, **kwargs):
|
||||
|
|
@ -51,107 +72,140 @@ class LivechatController(http.Controller):
|
|||
info = channel.get_livechat_info(username=username)
|
||||
return request.render('im_livechat.loader', {'info': info}, headers=[('Content-Type', 'application/javascript')])
|
||||
|
||||
@http.route('/im_livechat/init', type='json', auth="public", cors="*")
|
||||
def livechat_init(self, channel_id):
|
||||
operator_available = len(request.env['im_livechat.channel'].sudo().browse(channel_id)._get_available_users())
|
||||
rule = {}
|
||||
# find the country from the request
|
||||
country_id = False
|
||||
country_code = request.geoip.get('country_code')
|
||||
if country_code:
|
||||
country_id = request.env['res.country'].sudo().search([('code', '=', country_code)], limit=1).id
|
||||
# extract url
|
||||
url = request.httprequest.headers.get('Referer')
|
||||
# find the first matching rule for the given country and url
|
||||
matching_rule = request.env['im_livechat.channel.rule'].sudo().match_rule(channel_id, url, country_id)
|
||||
if matching_rule and (not matching_rule.chatbot_script_id or matching_rule.chatbot_script_id.script_step_ids):
|
||||
frontend_lang = request.httprequest.cookies.get('frontend_lang', request.env.user.lang or 'en_US')
|
||||
matching_rule = matching_rule.with_context(lang=frontend_lang)
|
||||
rule = {
|
||||
'action': matching_rule.action,
|
||||
'auto_popup_timer': matching_rule.auto_popup_timer,
|
||||
'regex_url': matching_rule.regex_url,
|
||||
def _process_extra_channel_params(self, **kwargs):
|
||||
# non_persisted_channel_params, persisted_channel_params
|
||||
return {}, {}
|
||||
|
||||
def _get_guest_name(self):
|
||||
return _("Visitor")
|
||||
|
||||
@http.route('/im_livechat/get_session', methods=["POST"], type="jsonrpc", auth='public')
|
||||
@add_guest_to_context
|
||||
def get_session(self, channel_id, previous_operator_id=None, chatbot_script_id=None, persisted=True, **kwargs):
|
||||
channel = request.env["discuss.channel"]
|
||||
country = request.env["res.country"]
|
||||
guest = request.env["mail.guest"]
|
||||
store = Store()
|
||||
livechat_channel = (
|
||||
request.env["im_livechat.channel"]
|
||||
.with_context(lang=False)
|
||||
.sudo()
|
||||
.search([("id", "=", channel_id)])
|
||||
)
|
||||
if not livechat_channel:
|
||||
raise NotFound()
|
||||
if not request.env.user._is_public():
|
||||
country = request.env.user.country_id
|
||||
elif request.geoip.country_code:
|
||||
country = request.env["res.country"].search(
|
||||
[("code", "=", request.geoip.country_code)], limit=1
|
||||
)
|
||||
operator_info = livechat_channel._get_operator_info(
|
||||
previous_operator_id=previous_operator_id,
|
||||
chatbot_script_id=chatbot_script_id,
|
||||
country_id=country.id,
|
||||
lang=request.cookies.get("frontend_lang"),
|
||||
**kwargs
|
||||
)
|
||||
if not operator_info['operator_partner']:
|
||||
return False
|
||||
|
||||
chatbot_script = operator_info['chatbot_script']
|
||||
is_chatbot_script = operator_info['operator_model'] == 'chatbot.script'
|
||||
non_persisted_channel_params, persisted_channel_params = self._process_extra_channel_params(**kwargs)
|
||||
|
||||
if not persisted:
|
||||
channel_id = -1 # only one temporary thread at a time, id does not matter.
|
||||
chatbot_data = None
|
||||
if is_chatbot_script:
|
||||
welcome_steps = chatbot_script._get_welcome_steps()
|
||||
chatbot_data = {
|
||||
"script": chatbot_script.id,
|
||||
"steps": welcome_steps.mapped(lambda s: {"scriptStep": s.id}),
|
||||
}
|
||||
store.add(chatbot_script)
|
||||
store.add(welcome_steps)
|
||||
channel_info = {
|
||||
"fetchChannelInfoState": "fetched",
|
||||
"id": channel_id,
|
||||
"isLoaded": True,
|
||||
"livechat_operator_id": Store.One(
|
||||
operator_info["operator_partner"], self.env["discuss.channel"]._store_livechat_operator_id_fields(),
|
||||
),
|
||||
"scrollUnread": False,
|
||||
"channel_type": "livechat",
|
||||
"chatbot": chatbot_data,
|
||||
**non_persisted_channel_params,
|
||||
}
|
||||
if matching_rule.chatbot_script_id.active and (not matching_rule.chatbot_only_if_no_operator or
|
||||
(not operator_available and matching_rule.chatbot_only_if_no_operator)) and matching_rule.chatbot_script_id.script_step_ids:
|
||||
chatbot_script = matching_rule.chatbot_script_id
|
||||
rule.update({'chatbot': chatbot_script._format_for_frontend()})
|
||||
store.add_model_values("discuss.channel", channel_info)
|
||||
else:
|
||||
if request.env.user._is_public():
|
||||
guest = guest.sudo()._get_or_create_guest(
|
||||
guest_name=self._get_guest_name(),
|
||||
country_code=request.geoip.country_code,
|
||||
timezone=request.env["mail.guest"]._get_timezone_from_request(request),
|
||||
)
|
||||
livechat_channel = livechat_channel.with_context(guest=guest)
|
||||
request.update_context(guest=guest)
|
||||
channel_vals = livechat_channel._get_livechat_discuss_channel_vals(**operator_info)
|
||||
channel_vals.update(**persisted_channel_params)
|
||||
lang = request.env["res.lang"].search(
|
||||
[("code", "=", request.cookies.get("frontend_lang"))]
|
||||
)
|
||||
channel_vals.update({"country_id": country.id, "livechat_lang_id": lang.id})
|
||||
channel = request.env['discuss.channel'].with_context(
|
||||
lang=request.env['chatbot.script']._get_chatbot_language()
|
||||
).sudo().create(channel_vals)
|
||||
channel_id = channel.id
|
||||
if is_chatbot_script:
|
||||
chatbot_script._post_welcome_steps(channel)
|
||||
if not is_chatbot_script or chatbot_script.operator_partner_id != channel.livechat_operator_id:
|
||||
channel._broadcast([channel.livechat_operator_id.id])
|
||||
if guest:
|
||||
store.add_global_values(guest_token=guest.sudo()._format_auth_cookie())
|
||||
request.env["res.users"]._init_store_data(store)
|
||||
# Make sure not to send "isLoaded" value on the guest bus, otherwise it
|
||||
# could be overwritten.
|
||||
if channel:
|
||||
store.add(
|
||||
channel,
|
||||
extra_fields={
|
||||
"isLoaded": not is_chatbot_script,
|
||||
"scrollUnread": False,
|
||||
},
|
||||
)
|
||||
if not request.env.user._is_public():
|
||||
store.add(
|
||||
request.env.user.partner_id,
|
||||
{"email": request.env.user.partner_id.email},
|
||||
)
|
||||
return {
|
||||
'available_for_me': (rule and rule.get('chatbot'))
|
||||
or operator_available and (not rule or rule['action'] != 'hide_button'),
|
||||
'rule': rule,
|
||||
"store_data": store.get_result(),
|
||||
"channel_id": channel_id,
|
||||
}
|
||||
|
||||
@http.route('/im_livechat/operator/<int:operator_id>/avatar',
|
||||
type='http', auth="public", cors="*")
|
||||
def livechat_operator_get_avatar(self, operator_id):
|
||||
""" Custom route allowing to retrieve an operator's avatar.
|
||||
|
||||
This is done to bypass more complicated rules, notably 'website_published' when the website
|
||||
module is installed.
|
||||
|
||||
Here, we assume that if you are a member of at least one im_livechat.channel, then it's ok
|
||||
to make your avatar publicly available.
|
||||
|
||||
We also make the chatbot operator avatars publicly available. """
|
||||
|
||||
is_livechat_member = False
|
||||
operator = request.env['res.partner'].sudo().browse(operator_id)
|
||||
if operator.exists():
|
||||
is_livechat_member = bool(request.env['im_livechat.channel'].sudo().search_count([
|
||||
('user_ids', 'in', operator.user_ids.ids)
|
||||
]))
|
||||
|
||||
if not is_livechat_member:
|
||||
# we don't put chatbot operators as livechat members (because we don't have a user_id for them)
|
||||
is_livechat_member = bool(request.env['chatbot.script'].sudo().search_count([
|
||||
('operator_partner_id', 'in', operator.ids)
|
||||
]))
|
||||
|
||||
return request.env['ir.binary']._get_image_stream_from(
|
||||
operator if is_livechat_member else request.env['res.partner'],
|
||||
field_name='avatar_128',
|
||||
placeholder='mail/static/src/img/smiley/avatar.jpg',
|
||||
).get_response()
|
||||
|
||||
@http.route('/im_livechat/get_session', type="json", auth='public', cors="*")
|
||||
def get_session(self, channel_id, anonymous_name, previous_operator_id=None, chatbot_script_id=None, persisted=True, **kwargs):
|
||||
user_id = None
|
||||
country_id = None
|
||||
# if the user is identifiy (eg: portal user on the frontend), don't use the anonymous name. The user will be added to session.
|
||||
if request.session.uid:
|
||||
user_id = request.env.user.id
|
||||
country_id = request.env.user.country_id.id
|
||||
else:
|
||||
# if geoip, add the country name to the anonymous name
|
||||
if request.geoip:
|
||||
# get the country of the anonymous person, if any
|
||||
country_code = request.geoip.get('country_code', "")
|
||||
country = request.env['res.country'].sudo().search([('code', '=', country_code)], limit=1) if country_code else None
|
||||
if country:
|
||||
country_id = country.id
|
||||
|
||||
if previous_operator_id:
|
||||
previous_operator_id = int(previous_operator_id)
|
||||
|
||||
chatbot_script = False
|
||||
if chatbot_script_id:
|
||||
frontend_lang = request.httprequest.cookies.get('frontend_lang', request.env.user.lang or 'en_US')
|
||||
chatbot_script = request.env['chatbot.script'].sudo().with_context(lang=frontend_lang).browse(chatbot_script_id)
|
||||
|
||||
return request.env["im_livechat.channel"].with_context(lang=False).sudo().browse(channel_id)._open_livechat_mail_channel(
|
||||
anonymous_name,
|
||||
previous_operator_id=previous_operator_id,
|
||||
chatbot_script=chatbot_script,
|
||||
user_id=user_id,
|
||||
country_id=country_id,
|
||||
persisted=persisted
|
||||
def _post_feedback_message(self, channel, rating, reason):
|
||||
body = Markup(
|
||||
"""<div class="o_mail_notification o_hide_author">"""
|
||||
"""%(rating)s: <img class="o_livechat_emoji_rating" src="%(rating_url)s" alt="rating"/>%(reason)s"""
|
||||
"""</div>"""
|
||||
) % {
|
||||
"rating": _("Rating"),
|
||||
"rating_url": rating.rating_image_url,
|
||||
"reason": nl2br("\n" + reason) if reason else "",
|
||||
}
|
||||
# sudo: discuss.channel - not necessary for posting, but necessary to update related rating
|
||||
channel.sudo().message_post(
|
||||
body=body,
|
||||
message_type="notification",
|
||||
rating_id=rating.id,
|
||||
subtype_xmlid="mail.mt_comment",
|
||||
)
|
||||
|
||||
@http.route('/im_livechat/feedback', type='json', auth='public', cors="*")
|
||||
def feedback(self, uuid, rate, reason=None, **kwargs):
|
||||
channel = request.env['mail.channel'].sudo().search([('uuid', '=', uuid)], limit=1)
|
||||
if channel:
|
||||
@http.route("/im_livechat/feedback", type="jsonrpc", auth="public")
|
||||
@add_guest_to_context
|
||||
def feedback(self, channel_id, rate, reason=None, **kwargs):
|
||||
if channel := request.env["discuss.channel"].search([("id", "=", channel_id)]):
|
||||
# limit the creation : only ONE rating per session
|
||||
values = {
|
||||
'rating': rate,
|
||||
|
|
@ -159,13 +213,15 @@ class LivechatController(http.Controller):
|
|||
'feedback': reason,
|
||||
'is_internal': False,
|
||||
}
|
||||
if not channel.rating_ids:
|
||||
# sudo: rating.rating - visitor can access rating to check if
|
||||
# feedback was already given
|
||||
if not channel.sudo().rating_ids:
|
||||
values.update({
|
||||
'res_id': channel.id,
|
||||
'res_model_id': request.env['ir.model']._get_id('mail.channel'),
|
||||
'res_model_id': request.env['ir.model']._get_id('discuss.channel'),
|
||||
})
|
||||
# find the partner (operator)
|
||||
if channel.channel_partner_ids:
|
||||
# sudo: res.partner - visitor must find the operator to rate
|
||||
if channel.sudo().channel_partner_ids:
|
||||
values['rated_partner_id'] = channel.channel_partner_ids[0].id
|
||||
# if logged in user, set its partner on rating
|
||||
values['partner_id'] = request.env.user.partner_id.id if request.session.uid else False
|
||||
|
|
@ -173,46 +229,57 @@ class LivechatController(http.Controller):
|
|||
rating = request.env['rating.rating'].sudo().create(values)
|
||||
else:
|
||||
rating = channel.rating_ids[0]
|
||||
rating.write(values)
|
||||
# sudo: rating.rating - guest or portal user can update their livechat rating
|
||||
rating.sudo().write(values)
|
||||
self._post_feedback_message(channel, rating, reason)
|
||||
return rating.id
|
||||
return False
|
||||
|
||||
@http.route('/im_livechat/history', type="json", auth="public", cors="*")
|
||||
def history_pages(self, pid, channel_uuid, page_history=None):
|
||||
partner_ids = (pid, request.env.user.partner_id.id)
|
||||
channel = request.env['mail.channel'].sudo().search([('uuid', '=', channel_uuid), ('channel_partner_ids', 'in', partner_ids)])
|
||||
if channel:
|
||||
channel._send_history_message(pid, page_history)
|
||||
return True
|
||||
@http.route("/im_livechat/history", type="jsonrpc", auth="public")
|
||||
@add_guest_to_context
|
||||
def history_pages(self, pid, channel_id, page_history=None):
|
||||
if channel := request.env["discuss.channel"].search([("id", "=", channel_id)]):
|
||||
if pid in channel.sudo().channel_member_ids.partner_id.ids:
|
||||
request.env["res.partner"].browse(pid)._bus_send_history_message(channel, page_history)
|
||||
|
||||
@http.route('/im_livechat/notify_typing', type='json', auth='public', cors="*")
|
||||
def notify_typing(self, uuid, is_typing):
|
||||
""" Broadcast the typing notification of the website user to other channel members
|
||||
:param uuid: (string) the UUID of the livechat channel
|
||||
:param is_typing: (boolean) tells whether the website user is typing or not.
|
||||
"""
|
||||
channel = request.env['mail.channel'].sudo().search([('uuid', '=', uuid)])
|
||||
if not channel:
|
||||
@http.route("/im_livechat/email_livechat_transcript", type="jsonrpc", auth="user")
|
||||
@add_guest_to_context
|
||||
def email_livechat_transcript(self, channel_id, email):
|
||||
if not request.env.user._is_internal():
|
||||
raise NotFound()
|
||||
channel_member = channel.env['mail.channel.member'].search([('channel_id', '=', channel.id), ('partner_id', '=', request.env.user.partner_id.id)])
|
||||
if not channel_member:
|
||||
raise NotFound()
|
||||
channel_member._notify_typing(is_typing=is_typing)
|
||||
|
||||
@http.route('/im_livechat/email_livechat_transcript', type='json', auth='public', cors="*")
|
||||
def email_livechat_transcript(self, uuid, email):
|
||||
channel = request.env['mail.channel'].sudo().search([
|
||||
('channel_type', '=', 'livechat'),
|
||||
('uuid', '=', uuid)], limit=1)
|
||||
if channel:
|
||||
if channel := request.env["discuss.channel"].search([("id", "=", channel_id)]):
|
||||
channel._email_livechat_transcript(email)
|
||||
|
||||
@http.route('/im_livechat/visitor_leave_session', type='json', auth="public")
|
||||
def visitor_leave_session(self, uuid):
|
||||
""" Called when the livechat visitor leaves the conversation.
|
||||
This will clean the chat request and warn the operator that the conversation is over.
|
||||
This allows also to re-send a new chat request to the visitor, as while the visitor is
|
||||
in conversation with an operator, it's not possible to send the visitor a chat request."""
|
||||
mail_channel = request.env['mail.channel'].sudo().search([('uuid', '=', uuid)])
|
||||
if mail_channel:
|
||||
mail_channel._close_livechat_session()
|
||||
@http.route("/im_livechat/download_transcript/<int:channel_id>", type="http", auth="public")
|
||||
@add_guest_to_context
|
||||
def download_livechat_transcript(self, channel_id):
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel:
|
||||
raise NotFound()
|
||||
partner, guest = request.env["res.partner"]._get_current_persona()
|
||||
tz = timezone(partner.tz or guest.timezone or "UTC")
|
||||
pdf, _type = (
|
||||
request.env["ir.actions.report"]
|
||||
.sudo()
|
||||
._render_qweb_pdf(
|
||||
"im_livechat.action_report_livechat_conversation",
|
||||
channel.ids,
|
||||
data={"company": request.env.company, "tz": tz},
|
||||
)
|
||||
)
|
||||
headers = [
|
||||
("Content-Disposition", content_disposition(f"transcript_{channel.id}.pdf", "inline")),
|
||||
("Content-Length", len(pdf)),
|
||||
("Content-Type", "application/pdf"),
|
||||
]
|
||||
return request.make_response(pdf, headers=headers)
|
||||
|
||||
@http.route("/im_livechat/visitor_leave_session", type="jsonrpc", auth="public")
|
||||
@add_guest_to_context
|
||||
def visitor_leave_session(self, channel_id):
|
||||
"""Called when the livechat visitor leaves the conversation.
|
||||
This will clean the chat request and warn the operator that the conversation is over.
|
||||
This allows also to re-send a new chat request to the visitor, as while the visitor is
|
||||
in conversation with an operator, it's not possible to send the visitor a chat request."""
|
||||
if channel := request.env["discuss.channel"].search([("id", "=", channel_id)]):
|
||||
channel._close_livechat_session()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from odoo.http import route, request
|
||||
from odoo.addons.mail.controllers.discuss.rtc import RtcController
|
||||
from odoo.addons.mail.tools.discuss import add_guest_to_context
|
||||
|
||||
|
||||
class LivechatRtcController(RtcController):
|
||||
@route()
|
||||
@add_guest_to_context
|
||||
def channel_call_join(self, channel_id, check_rtc_session_ids=None, camera=False):
|
||||
# sudo: discuss.channel - visitor can check if there is an ongoing call
|
||||
if not request.env.user._is_internal() and request.env["discuss.channel"].sudo().search([
|
||||
("id", "=", channel_id),
|
||||
("channel_type", "=", "livechat"),
|
||||
("rtc_session_ids", "=", False),
|
||||
]):
|
||||
raise NotFound()
|
||||
return super().channel_call_join(channel_id, check_rtc_session_ids, camera)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.http import request, route
|
||||
from odoo.addons.mail.controllers import thread
|
||||
|
||||
|
||||
class ThreadController(thread.ThreadController):
|
||||
@route()
|
||||
def mail_message_post(self, thread_model, thread_id, post_data, context=None, **kwargs):
|
||||
if selected_answer_id := kwargs.pop("selected_answer_id", None):
|
||||
request.update_context(selected_answer_id=selected_answer_id)
|
||||
return super().mail_message_post(thread_model, thread_id, post_data, context, **kwargs)
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.http import request, route
|
||||
from odoo.addons.mail.controllers.webclient import WebclientController
|
||||
from odoo.addons.mail.tools.discuss import Store
|
||||
|
||||
|
||||
class WebClient(WebclientController):
|
||||
@route("/web/tests/livechat", type="http", auth="user")
|
||||
def test_external_livechat(self, **kwargs):
|
||||
return request.render(
|
||||
"im_livechat.unit_embed_suite",
|
||||
{
|
||||
"server_url": request.env["ir.config_parameter"].get_base_url(),
|
||||
"session_info": {"view_info": request.env["ir.ui.view"].get_view_info()},
|
||||
},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _process_request_for_internal_user(self, store: Store, name, params):
|
||||
super()._process_request_for_internal_user(store, name, params)
|
||||
if name == "im_livechat.channel":
|
||||
store.add(request.env["im_livechat.channel"].search([]), ["are_you_inside", "name"])
|
||||
if name == "/im_livechat/looking_for_help":
|
||||
chats_looking_for_help = request.env["discuss.channel"].search(
|
||||
[("livechat_status", "=", "need_help")], order="id ASC", limit=100
|
||||
)
|
||||
request.update_context(
|
||||
channels=request.env.context["channels"] | chats_looking_for_help
|
||||
)
|
||||
if name == "/im_livechat/session/data":
|
||||
channel_id = params.get("channel_id")
|
||||
if not channel_id:
|
||||
return
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel:
|
||||
return
|
||||
fields_to_store = channel._get_livechat_session_fields_to_store()
|
||||
store.add(channel, fields=fields_to_store)
|
||||
if name == "/im_livechat/fetch_self_expertise":
|
||||
store.add(request.env.user, Store.Many("livechat_expertise_ids", ["name"]))
|
||||
|
||||
@classmethod
|
||||
def _process_request_for_all(self, store: Store, name, params):
|
||||
super()._process_request_for_all(store, name, params)
|
||||
if name == "init_livechat":
|
||||
partner, guest = request.env["res.partner"]._get_current_persona()
|
||||
if partner:
|
||||
store.add_global_values(self_partner=Store.One(partner, extra_fields="email"))
|
||||
if guest:
|
||||
store.add_global_values(self_guest=Store.One(guest))
|
||||
# sudo - im_livechat.channel: allow access to live chat channel to
|
||||
# check if operators are available.
|
||||
channel = request.env["im_livechat.channel"].sudo().search([("id", "=", params)])
|
||||
if not channel:
|
||||
return
|
||||
country_id = (
|
||||
# sudo - res.country: accessing user country is allowed.
|
||||
request.env["res.country"].sudo().search([("code", "=", code)]).id
|
||||
if (code := request.geoip.country_code)
|
||||
else None
|
||||
)
|
||||
url = request.httprequest.headers.get("Referer")
|
||||
if (
|
||||
# sudo - im_livechat.channel.rule: getting channel's rule is allowed.
|
||||
matching_rule := request.env["im_livechat.channel.rule"]
|
||||
.sudo()
|
||||
.match_rule(params, url, country_id)
|
||||
):
|
||||
matching_rule = matching_rule.with_context(
|
||||
lang=request.env["chatbot.script"]._get_chatbot_language(),
|
||||
)
|
||||
store.add_global_values(livechat_rule=Store.One(matching_rule))
|
||||
store.add_global_values(
|
||||
livechat_available=matching_rule.action != "hide_button"
|
||||
and bool(matching_rule._is_bot_configured() or channel.available_operator_ids),
|
||||
can_download_transcript=bool(
|
||||
request.env.ref("im_livechat.action_report_livechat_conversation", False),
|
||||
),
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue