# Part of Odoo. See LICENSE file for full copyright and licensing details. import pytz import uuid from datetime import datetime, timedelta from functools import wraps from inspect import Parameter, signature import odoo from odoo.tools import consteq, get_lang from odoo import _, api, fields, models from odoo.http import request from odoo.addons.base.models.res_partner import _tz_get from odoo.exceptions import UserError from odoo.addons.bus.models.bus_presence import AWAY_TIMER, DISCONNECTION_TIMER from odoo.addons.bus.websocket import wsrequest def add_guest_to_context(func): """ Decorate a function to extract the guest from the request. The guest is then available on the context of the current request. """ @wraps(func) def wrapper(self, *args, **kwargs): req = request or wsrequest token = ( req.httprequest.cookies.get(req.env["mail.guest"]._cookie_name, "") ) guest = req.env["mail.guest"]._get_guest_from_token(token) if guest and not guest.timezone: timezone = req.env["mail.guest"]._get_timezone_from_request(req) if timezone: guest._update_timezone(timezone) if guest: req.update_context(guest=guest) if hasattr(self, "env"): self.env.context = {**self.env.context, "guest": guest} return func(self, *args, **kwargs) return wrapper class MailGuest(models.Model): _name = 'mail.guest' _description = "Guest" _inherit = ['avatar.mixin'] _avatar_name_field = "name" _cookie_name = 'dgid' _cookie_separator = '|' @api.model def _lang_get(self): return self.env['res.lang'].get_installed() name = fields.Char(string="Name", required=True) access_token = fields.Char(string="Access Token", default=lambda self: str(uuid.uuid4()), groups='base.group_system', required=True, readonly=True, copy=False) country_id = fields.Many2one(string="Country", comodel_name='res.country') lang = fields.Selection(string="Language", selection=_lang_get) timezone = fields.Selection(string="Timezone", selection=_tz_get) channel_ids = fields.Many2many(string="Channels", comodel_name='discuss.channel', relation='discuss_channel_member', column1='guest_id', column2='channel_id', copy=False) im_status = fields.Char('IM Status', compute='_compute_im_status') def _compute_im_status(self): self.env.cr.execute(""" SELECT guest_id as id, CASE WHEN age(now() AT TIME ZONE 'UTC', last_poll) > interval %s THEN 'offline' WHEN age(now() AT TIME ZONE 'UTC', last_presence) > interval %s THEN 'away' ELSE 'online' END as status FROM bus_presence WHERE guest_id IN %s """, ("%s seconds" % DISCONNECTION_TIMER, "%s seconds" % AWAY_TIMER, tuple(self.ids))) res = dict(((status['id'], status['status']) for status in self.env.cr.dictfetchall())) for guest in self: guest.im_status = res.get(guest.id, 'offline') def _get_guest_from_token(self, token=""): """Returns the guest record for the given token, if applicable.""" guest = self.env["mail.guest"] parts = token.split(self._cookie_separator) if len(parts) == 2: guest_id, guest_access_token = parts # sudo: mail.guest: guests need sudo to read their access_token guest = self.browse(int(guest_id)).sudo().exists() if not guest or not guest.access_token or not consteq(guest.access_token, guest_access_token): guest = self.env["mail.guest"] return guest.sudo(False) def _get_guest_from_context(self): """Returns the current guest record from the context, if applicable.""" guest = self.env.context.get('guest') if isinstance(guest, self.pool['mail.guest']): return guest.sudo(False).with_context(guest=guest) return self.env['mail.guest'] def _get_timezone_from_request(self, request): timezone = request.httprequest.cookies.get('tz') return timezone if timezone in pytz.all_timezones else False def _update_name(self, name): self.ensure_one() name = name.strip() if len(name) < 1: raise UserError(_("Guest's name cannot be empty.")) if len(name) > 512: raise UserError(_("Guest's name is too long.")) self.name = name guest_data = { 'id': self.id, 'name': self.name, 'type': "guest" } bus_notifs = [(channel, 'mail.record/insert', {'Persona': guest_data}) for channel in self.channel_ids] bus_notifs.append((self, 'mail.record/insert', {'Persona': guest_data})) self.env['bus.bus']._sendmany(bus_notifs) def _update_timezone(self, timezone): query = """ UPDATE mail_guest SET timezone = %s WHERE id IN ( SELECT id FROM mail_guest WHERE id = %s FOR NO KEY UPDATE SKIP LOCKED ) """ self.env.cr.execute(query, (timezone, self.id)) def _init_messaging(self): self.ensure_one() # sudo: res.partner - exposing TalismanBot name and id odoobot = self.env.ref('base.partner_root').sudo() # sudo: mail.guest - guest reading their own id/name/channels guest_sudo = self.sudo() return { 'channels': guest_sudo.channel_ids.sudo(False)._channel_info(), 'companyName': self.env.company.name, 'currentGuest': { 'id': guest_sudo.id, 'name': guest_sudo.name, 'type': "guest", }, 'current_partner': False, 'current_user_id': False, 'current_user_settings': False, # sudo: ir.config_parameter: safe to check for existence of tenor api key 'hasGifPickerFeature': bool(self.env["ir.config_parameter"].sudo().get_param("discuss.tenor_api_key")), 'hasLinkPreviewFeature': self.env['mail.link.preview']._is_link_preview_enabled(), 'hasMessageTranslationFeature': False, # sudo: bus.bus: reading non-sensitive last id 'initBusId': self.env['bus.bus'].sudo()._bus_last_id(), 'menu_id': False, 'needaction_inbox_counter': False, 'odoobot': { 'id': odoobot.id, 'name': odoobot.name, 'type': "partner", }, 'shortcodes': [], 'starred_counter': False, } def _guest_format(self, fields=None): if not fields: fields = {'id': True, 'name': True, 'im_status': True, "write_date": True} guests_formatted_data = {} for guest in self: data = {} if 'id' in fields: data['id'] = guest.id if 'name' in fields: data['name'] = guest.name if 'im_status' in fields: data['im_status'] = guest.im_status if "write_date" in fields: data["write_date"] = odoo.fields.Datetime.to_string(guest.write_date) data['type'] = "guest" guests_formatted_data[guest] = data return guests_formatted_data def _set_auth_cookie(self): """Add a cookie to the response to identify the guest. Every route that expects a guest will make use of it to authenticate the guest through `add_guest_to_context`. """ self.ensure_one() expiration_date = datetime.now() + timedelta(days=365) request.future_response.set_cookie( self._cookie_name, self._format_auth_cookie(), httponly=True, expires=expiration_date, ) request.update_context(guest=self.sudo(False)) def _format_auth_cookie(self): """Format the cookie value for the given guest. :param guest: guest to format the cookie value for :return str: formatted cookie value """ self.ensure_one() return f"{self.id}{self._cookie_separator}{self.access_token}"