* = crm_livechat, hr, hr_holidays, im_livechat, mail_bot, purchase, sms,
snailmail, survey, test_discuss_full, test_mail, web_editor, website,
website_livechat
- Create new model `mail.guest` for guests.
- Rewrite some RPCs to target routes rather than model methods so that
guests are able to use them.
- Patch JS and python models to support guests.
- Create a stand-alone page and boot the channel in it.
task-2494829
closes odoo/odoo#75496
Related: odoo/enterprise#20417
Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
288 lines
17 KiB
Python
288 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import json
|
|
|
|
from odoo import http
|
|
from odoo.http import request
|
|
from odoo.tools import consteq
|
|
from odoo.tools.misc import get_lang
|
|
from odoo.tools.translate import _
|
|
from werkzeug.exceptions import NotFound
|
|
|
|
|
|
class DiscussController(http.Controller):
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Public Pages
|
|
# --------------------------------------------------------------------------
|
|
|
|
@http.route('/chat/<int:channel_id>/<string:invitation_token>', methods=['GET'], type='http', auth='public')
|
|
def discuss_channel_invitation(self, channel_id, invitation_token, **kwargs):
|
|
channel_sudo = request.env['mail.channel'].browse(int(channel_id)).sudo().exists()
|
|
if not channel_sudo or not channel_sudo.uuid or not consteq(channel_sudo.uuid, invitation_token):
|
|
raise NotFound()
|
|
response = request.redirect(f'/discuss/channel/{channel_sudo.id}')
|
|
if request.env['mail.channel.partner']._get_as_sudo_from_request(request=request, channel_id=int(channel_id)):
|
|
return response
|
|
if channel_sudo.channel_type == 'chat':
|
|
raise NotFound()
|
|
if request.session.uid:
|
|
channel_sudo.add_members([request.env.user.partner_id.id])
|
|
return response
|
|
guest = request.env['mail.guest']._get_guest_from_request(request)
|
|
if not guest:
|
|
guest = request.env['mail.guest'].sudo().create({
|
|
'country_id': request.env['res.country'].sudo().search([('code', '=', request.session.get('geoip', {}).get('country_code'))], limit=1).id,
|
|
'lang': get_lang(request.env).code,
|
|
'name': _("Guest"),
|
|
'timezone': request.env['mail.guest']._get_timezone_from_request(request),
|
|
})
|
|
# Guest session cookies, every route in this file will make use of them to authenticate
|
|
# the guest through `_get_as_sudo_from_request` or `_get_as_sudo_from_request_or_raise`.
|
|
response.set_cookie('mail.guest_id', str(guest.id), httponly=True)
|
|
response.set_cookie('mail.guest_access_token', guest.access_token, httponly=True)
|
|
channel_sudo.add_members(guest_ids=[guest.id])
|
|
return response
|
|
|
|
@http.route('/discuss/channel/<int:channel_id>', methods=['GET'], type='http', auth='public')
|
|
def discuss_channel(self, channel_id, **kwargs):
|
|
channel_partner_sudo = request.env['mail.channel.partner']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id))
|
|
return request.render('mail.discuss_public_channel_template', {
|
|
'channel_sudo': channel_partner_sudo.channel_id,
|
|
'session_info': channel_partner_sudo.env['ir.http'].session_info(),
|
|
})
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Semi-Static Content (GET requests with possible cache)
|
|
# --------------------------------------------------------------------------
|
|
|
|
@http.route('/mail/channel/<int:channel_id>/partner/<int:partner_id>/avatar_128', methods=['GET'], type='http', auth='public')
|
|
def mail_channel_partner_avatar_128(self, channel_id, partner_id, **kwargs):
|
|
channel_partner_sudo = request.env['mail.channel.partner']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id))
|
|
if not channel_partner_sudo.env['mail.channel.partner'].search([('channel_id', '=', int(channel_id)), ('partner_id', '=', int(partner_id))], limit=1):
|
|
raise NotFound()
|
|
return channel_partner_sudo.env['ir.http']._content_image(model='res.partner', res_id=int(partner_id), field='avatar_128')
|
|
|
|
@http.route('/mail/channel/<int:channel_id>/guest/<int:guest_id>/avatar_128', methods=['GET'], type='http', auth='public')
|
|
def mail_channel_guest_avatar_128(self, channel_id, guest_id, **kwargs):
|
|
channel_partner_sudo = request.env['mail.channel.partner']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id))
|
|
if not channel_partner_sudo.env['mail.channel.partner'].search([('channel_id', '=', int(channel_id)), ('guest_id', '=', int(guest_id))], limit=1):
|
|
raise NotFound()
|
|
return channel_partner_sudo.env['ir.http']._content_image(model='mail.guest', res_id=int(guest_id), field='avatar_128')
|
|
|
|
@http.route('/mail/channel/<int:channel_id>/attachment/<int:attachment_id>', methods=['GET'], type='http', auth='public')
|
|
def mail_channel_attachment(self, channel_id, attachment_id, **kwargs):
|
|
channel_partner_sudo = request.env['mail.channel.partner']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id))
|
|
if not channel_partner_sudo.env['ir.attachment'].search([('id', '=', int(attachment_id)), ('res_id', '=', int(channel_id)), ('res_model', '=', 'mail.channel')], limit=1):
|
|
raise NotFound()
|
|
return channel_partner_sudo.env['ir.http']._get_content_common(res_id=int(attachment_id), download=True)
|
|
|
|
@http.route('/mail/channel/<int:channel_id>/image/<int:attachment_id>/<int:width>x<int:height>', methods=['GET'], type='http', auth='public')
|
|
def fetch_image(self, channel_id, attachment_id, width, height, **kwargs):
|
|
channel_partner_sudo = request.env['mail.channel.partner']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id))
|
|
if not channel_partner_sudo.env['ir.attachment'].search([('id', '=', int(attachment_id)), ('res_id', '=', int(channel_id)), ('res_model', '=', 'mail.channel')], limit=1):
|
|
raise NotFound()
|
|
return channel_partner_sudo.env['ir.http']._content_image(res_id=int(attachment_id), height=int(height), width=int(width))
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Client Initialization
|
|
# --------------------------------------------------------------------------
|
|
|
|
@http.route('/mail/init_messaging', methods=['POST'], type='json', auth='public')
|
|
def mail_init_messaging(self, **kwargs):
|
|
if request.session.uid:
|
|
return request.env.user._init_messaging()
|
|
guest = request.env['mail.guest']._get_guest_from_request(request)
|
|
if guest:
|
|
return guest.sudo()._init_messaging()
|
|
raise NotFound()
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Mailbox
|
|
# --------------------------------------------------------------------------
|
|
|
|
@http.route('/mail/inbox/messages', methods=['POST'], type='json', auth='user')
|
|
def discuss_inbox_messages(self, max_id=None, min_id=None, limit=30, **kwargs):
|
|
return request.env['mail.message']._message_fetch(domain=[('needaction', '=', True)], max_id=max_id, min_id=min_id, limit=limit)
|
|
|
|
@http.route('/mail/history/messages', methods=['POST'], type='json', auth='user')
|
|
def discuss_history_messages(self, max_id=None, min_id=None, limit=30, **kwargs):
|
|
return request.env['mail.message']._message_fetch(domain=[('needaction', '=', False)], max_id=max_id, min_id=min_id, limit=limit)
|
|
|
|
@http.route('/mail/starred/messages', methods=['POST'], type='json', auth='user')
|
|
def discuss_starred_messages(self, max_id=None, min_id=None, limit=30, **kwargs):
|
|
return request.env['mail.message']._message_fetch(domain=[('starred_partner_ids', 'in', [request.env.user.partner_id.id])], max_id=max_id, min_id=min_id, limit=limit)
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Thread API (channel/chatter common)
|
|
# --------------------------------------------------------------------------
|
|
|
|
@http.route('/mail/message/post', methods=['POST'], type='json', auth='public')
|
|
def mail_message_post(self, thread_model, thread_id, post_data, **kwargs):
|
|
if thread_model == 'mail.channel':
|
|
channel_partner_sudo = request.env['mail.channel.partner']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(thread_id))
|
|
thread = channel_partner_sudo.channel_id
|
|
else:
|
|
thread = request.env[thread_model].browse(int(thread_id)).exists()
|
|
allowed_params = {'attachment_ids', 'body', 'message_type', 'partner_ids', 'subtype_xmlid'}
|
|
return thread.message_post(**{key: value for key, value in post_data.items() if key in allowed_params}).message_format()[0]
|
|
|
|
@http.route('/mail/attachment/upload', methods=['POST'], type='http', auth='public')
|
|
def mail_attachment_upload(self, ufile, thread_id, thread_model, is_pending=False, **kwargs):
|
|
channel_partner = request.env['mail.channel.partner']
|
|
if thread_model == 'mail.channel':
|
|
channel_partner = request.env['mail.channel.partner']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(thread_id))
|
|
vals = {
|
|
'name': ufile.filename,
|
|
'raw': ufile.read(),
|
|
'res_id': int(thread_id),
|
|
'res_model': thread_model,
|
|
}
|
|
if is_pending and is_pending != 'false':
|
|
# Add this point, the message related to the uploaded file does
|
|
# not exist yet, so we use those placeholder values instead.
|
|
vals.update({
|
|
'res_id': 0,
|
|
'res_model': 'mail.compose.message',
|
|
})
|
|
if channel_partner.env.user.share:
|
|
# Only generate the access token if absolutely necessary (= not for internal user).
|
|
vals['access_token'] = channel_partner.env['ir.attachment']._generate_access_token()
|
|
attachment = channel_partner.env['ir.attachment'].create(vals)
|
|
attachment._post_add_create()
|
|
attachmentData = {
|
|
'filename': ufile.filename,
|
|
'id': attachment.id,
|
|
'mimetype': attachment.mimetype,
|
|
'name': attachment.name,
|
|
'size': attachment.file_size
|
|
}
|
|
if attachment.access_token:
|
|
attachmentData['accessToken'] = attachment.access_token
|
|
return request.make_response(
|
|
data=json.dumps(attachmentData),
|
|
headers=[('Content-Type', 'application/json')]
|
|
)
|
|
|
|
@http.route('/mail/attachment/delete', methods=['POST'], type='json', auth='public')
|
|
def mail_attachment_delete(self, attachment_id, access_token=None, **kwargs):
|
|
attachment_sudo = request.env['ir.attachment'].browse(int(attachment_id)).sudo().exists()
|
|
if not attachment_sudo:
|
|
raise NotFound()
|
|
if not request.env.user.share:
|
|
# Check through standard access rights/rules for internal users.
|
|
return attachment_sudo.sudo(False).unlink()
|
|
# For non-internal users 2 cases are supported:
|
|
# - Either the attachment is linked to a message: verify the request is made by the author of the message (portal user or guest).
|
|
# - Either a valid access token is given: also verify the message is pending (because unfortunately in portal a token is also provided to guest for viewing others' attachments).
|
|
message_sudo = request.env['mail.message'].sudo().search([('attachment_ids', 'in', attachment_sudo.ids)], limit=1)
|
|
if message_sudo:
|
|
if request.session.uid and (not message_sudo.author_id or message_sudo.author_id != request.env.user.partner_id):
|
|
raise NotFound()
|
|
if not request.session.uid and (not message_sudo.author_guest_id or message_sudo.author_guest_id != request.env['mail.guest']._get_guest_from_request(request)):
|
|
raise NotFound()
|
|
else:
|
|
if not access_token or not attachment_sudo.access_token or not consteq(access_token, attachment_sudo.access_token):
|
|
raise NotFound()
|
|
if attachment_sudo.res_model != 'mail.compose.message' or attachment_sudo.res_id != 0:
|
|
raise NotFound()
|
|
return attachment_sudo.unlink()
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Channel API
|
|
# --------------------------------------------------------------------------
|
|
|
|
@http.route('/mail/channel/messages', methods=['POST'], type='json', auth='public')
|
|
def mail_channel_messages(self, channel_id, max_id=None, min_id=None, limit=30, **kwargs):
|
|
channel_partner_sudo = request.env['mail.channel.partner']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id))
|
|
return channel_partner_sudo.env['mail.message']._message_fetch(domain=[
|
|
('res_id', '=', channel_id),
|
|
('model', '=', 'mail.channel'),
|
|
('message_type', '!=', 'user_notification'),
|
|
], max_id=max_id, min_id=min_id, limit=limit)
|
|
|
|
@http.route('/mail/channel/set_last_seen_message', methods=['POST'], type='json', auth='public')
|
|
def mail_channel_mark_as_seen(self, channel_id, last_message_id, **kwargs):
|
|
channel_partner_sudo = request.env['mail.channel.partner']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id))
|
|
return channel_partner_sudo.channel_id._channel_seen(int(last_message_id))
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Chatter API
|
|
# --------------------------------------------------------------------------
|
|
|
|
@http.route('/mail/thread/messages', methods=['POST'], type='json', auth='user')
|
|
def mail_thread_messages(self, thread_model, thread_id, max_id=None, min_id=None, limit=30, **kwargs):
|
|
return request.env['mail.message']._message_fetch(domain=[
|
|
('res_id', '=', int(thread_id)),
|
|
('model', '=', thread_model),
|
|
('message_type', '!=', 'user_notification'),
|
|
], max_id=max_id, min_id=min_id, limit=limit)
|
|
|
|
@http.route('/mail/read_followers', methods=['POST'], type='json', auth='user')
|
|
def read_followers(self, res_model, res_id):
|
|
request.env['mail.followers'].check_access_rights("read")
|
|
request.env[res_model].check_access_rights("read")
|
|
request.env[res_model].browse(res_id).check_access_rule("read")
|
|
follower_recs = request.env['mail.followers'].search([('res_model', '=', res_model), ('res_id', '=', res_id)])
|
|
|
|
followers = []
|
|
follower_id = None
|
|
for follower in follower_recs:
|
|
if follower.partner_id == request.env.user.partner_id:
|
|
follower_id = follower.id
|
|
followers.append({
|
|
'id': follower.id,
|
|
'partner_id': follower.partner_id.id,
|
|
'name': follower.name,
|
|
'display_name': follower.display_name,
|
|
'email': follower.email,
|
|
'is_active': follower.is_active,
|
|
# When editing the followers, the "pencil" icon that leads to the edition of subtypes
|
|
# should be always be displayed and not only when "debug" mode is activated.
|
|
'is_editable': True
|
|
})
|
|
return {
|
|
'followers': followers,
|
|
'subtypes': self.read_subscription_data(follower_id) if follower_id else None
|
|
}
|
|
|
|
@http.route('/mail/read_subscription_data', methods=['POST'], type='json', auth='user')
|
|
def read_subscription_data(self, follower_id):
|
|
""" Computes:
|
|
- message_subtype_data: data about document subtypes: which are
|
|
available, which are followed if any """
|
|
request.env['mail.followers'].check_access_rights("read")
|
|
follower = request.env['mail.followers'].sudo().browse(follower_id)
|
|
follower.ensure_one()
|
|
request.env[follower.res_model].check_access_rights("read")
|
|
record = request.env[follower.res_model].browse(follower.res_id)
|
|
record.check_access_rule("read")
|
|
|
|
# find current model subtypes, add them to a dictionary
|
|
subtypes = record._mail_get_message_subtypes()
|
|
|
|
followed_subtypes_ids = set(follower.subtype_ids.ids)
|
|
subtypes_list = [{
|
|
'name': subtype.name,
|
|
'res_model': subtype.res_model,
|
|
'sequence': subtype.sequence,
|
|
'default': subtype.default,
|
|
'internal': subtype.internal,
|
|
'followed': subtype.id in followed_subtypes_ids,
|
|
'parent_model': subtype.parent_id.res_model,
|
|
'id': subtype.id
|
|
} for subtype in subtypes]
|
|
return sorted(subtypes_list,
|
|
key=lambda it: (it['parent_model'] or '', it['res_model'] or '', it['internal'], it['sequence']))
|
|
|
|
@http.route('/mail/get_suggested_recipients', methods=['POST'], type='json', auth='user')
|
|
def message_get_suggested_recipients(self, model, res_ids):
|
|
records = request.env[model].browse(res_ids)
|
|
try:
|
|
records.check_access_rule('read')
|
|
records.check_access_rights('read')
|
|
except Exception:
|
|
return {}
|
|
return records._message_get_suggested_recipients()
|