diff --git a/addons/mail/controllers/discuss.py b/addons/mail/controllers/discuss.py index 674942980d1..a4393cf9935 100644 --- a/addons/mail/controllers/discuss.py +++ b/addons/mail/controllers/discuss.py @@ -418,7 +418,7 @@ class DiscussController(http.Controller): :param int channel_id: id of the channel to join """ 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._join_call() + return channel_partner_sudo._rtc_join_call() @http.route('/mail/rtc/channel/leave_call', methods=['POST'], type="json", auth="public") def channel_call_leave(self, channel_id): @@ -426,7 +426,7 @@ class DiscussController(http.Controller): :param int channel_id: id of the channel from which to disconnect """ channel_partner_sudo = request.env['mail.channel.partner']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id)) - channel_partner_sudo.channel_id._leave_call() + return channel_partner_sudo._rtc_leave_call() @http.route('/mail/rtc/channel/cancel_call_invitation', methods=['POST'], type="json", auth="public") def channel_call_cancel_invitation(self, channel_id, partner_ids=None, guest_ids=None): @@ -437,7 +437,7 @@ class DiscussController(http.Controller): if either partner_ids or guest_ids is set, only the specified ids will be invited. """ channel_partner_sudo = request.env['mail.channel.partner']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id)) - channel_partner_sudo.channel_id._cancel_rtc_invitations(partner_ids=partner_ids, guest_ids=guest_ids, inviting_member=channel_partner_sudo) + return channel_partner_sudo.channel_id._rtc_cancel_invitations(partner_ids=partner_ids, guest_ids=guest_ids) @http.route('/mail/rtc/audio_worklet_processor', methods=['GET'], type='http', auth='public') def audio_worklet_processor(self): diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index 5ebed38e927..dc16f1b2def 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -294,8 +294,8 @@ class Channel(models.Model): """ Adds the given partner_ids and guest_ids as member of self channels. """ self.check_access_rights('write') self.check_access_rule('write') - partners = self.env['res.partner'].browse(partner_ids or []) - guests = self.env['mail.guest'].browse(guest_ids or []) + partners = self.env['res.partner'].browse(partner_ids or []).exists() + guests = self.env['mail.guest'].browse(guest_ids or []).exists() members_to_create = [] for channel in self: if channel.public == 'groups': @@ -318,8 +318,6 @@ class Channel(models.Model): 'channel_id': channel.id, } for partner in guests - existing_guests] new_members = self.env['mail.channel.partner'].sudo().create(members_to_create) - if invite_to_rtc_call: - self._invite_members_to_rtc(partner_ids=partner_ids, guest_ids=guest_ids) for channel_partner in new_members.filtered(lambda channel_partner: channel_partner.partner_id): user = channel_partner.partner_id.user_ids[0] if channel_partner.partner_id.user_ids else self.env['res.users'] # notify invited members through the bus @@ -352,9 +350,19 @@ class Channel(models.Model): 'id': channel_partner.channel_id.id, 'memberCount': channel_partner.channel_id.member_count, 'members': [('insert', new_partner_data)], - 'invitedPartners': [('insert', [{'id': new_partner_data['id']}] if invite_to_rtc_call else [])] }, }) + if invite_to_rtc_call: + if self.env.user._is_public() and 'guest' in self.env.context: + guest = self.env.context.get('guest') + partner = self.env['res.partner'] + else: + guest = self.env['mail.guest'] + partner = self.env.user.partner_id + for channel in self: + current_channel_partner = self.env['mail.channel.partner'].sudo().search([('channel_id', '=', channel.id), ('partner_id', '=', partner.id), ('guest_id', '=', guest.id)]) + if current_channel_partner and current_channel_partner.rtc_session_ids: + current_channel_partner._rtc_invite_members(partner_ids=partners.ids, guest_ids=guests.ids) def _action_remove_members(self, partners): """ Private implementation to remove members from channels. Done as sudo @@ -389,165 +397,53 @@ class Channel(models.Model): # RTC # ------------------------------------------------------------ - def _cancel_rtc_invitations(self, partner_ids=None, guest_ids=None, inviting_member=None): + def _rtc_cancel_invitations(self, partner_ids=None, guest_ids=None): """ Cancels the invitations of the RTC call from all invited members (or the specified partner_ids). :param list partner_ids: list of the partner ids from which the invitation has to be removed :param list guest_ids: list of the guest ids from which the invitation has to be removed if either partner_ids or guest_ids is set, only the specified ids will be invited. - :param inviting_member: if specified, only removes invitations if they are coming - from this channel member """ self.ensure_one() - notifications = [] - domain = ['&', ('rtc_inviting_session_id', '!=', False), ('channel_id', '=', self.id)] - if inviting_member: - domain = expression.AND([domain, [('rtc_inviting_session_id.channel_partner_id', '=', inviting_member.id)]]) + channel_partner_domain = [ + ('channel_id', '=', self.id), + ('rtc_inviting_session_id', '!=', False), + ] if partner_ids or guest_ids: - domain = expression.AND([domain, ['|', ('partner_id', 'in', partner_ids or []), ('guest_id', 'in', guest_ids or [])]]) - channel_partners = self.env['mail.channel.partner'].search(domain) - for member in channel_partners: - member.write({'rtc_inviting_session_id': False}) - model, record_id = ('mail.guest', member.guest_id.id) if member.guest_id else ('res.partner', member.partner_id.id) - notifications.append([ - (self._cr.dbname, model, record_id), - { - 'type': 'rtc_incoming_invitation_update', - 'payload': { - 'channelId': self.id, - }, - }, - ]) - self.env['bus.bus'].sendmany(notifications) - - def _invite_members_to_rtc(self, partner_ids=None, guest_ids=None): - """ Sends invitations to join the RTC call to all connected members of the thread who are not already invited. - :param list partner_ids: list of the partner ids to invite - :param list guest_ids: list of the guest ids to invite - - if either partner_ids or guest_ids is set, only the specified ids will be invited. - """ - self.ensure_one() - - if self.env.user._is_public(): - guest = self.env.context.get('guest') - partner = self.env['res.partner'] - else: - guest = self.env['mail.guest'] - partner = self.env.user.partner_id - - def is_current_user(r): - return (guest and r.guest_id == guest) or r.partner_id == partner - - current_rtc_session = self.rtc_session_ids.filtered(is_current_user) - if not current_rtc_session: - return [], [] - - current_sessions_channel_partners = self.rtc_session_ids.channel_partner_id - notifications = [] - invited_partners = [] - invited_guests = [] - domain = ['&', '&', ('rtc_inviting_session_id', '=', False), ('channel_id', '=', self.id), ('id', 'not in', current_sessions_channel_partners.ids)] - if partner: - domain = expression.AND([domain, [('partner_id', '!=', partner.id)]]) - if guest: - domain = expression.AND([domain, [('guest_id', '!=', guest.id)]]) - if partner_ids or guest_ids: - domain = expression.AND([domain, ['|', ('partner_id', 'in', partner_ids or []), ('guest_id', 'in', guest_ids or [])]]) - channel_partners = self.env['mail.channel.partner'].search(domain) - for member in channel_partners: - member.rtc_inviting_session_id = current_rtc_session.id + channel_partner_domain = expression.AND([channel_partner_domain, [ + '|', + ('partner_id', 'in', partner_ids or []), + ('guest_id', 'in', guest_ids or []), + ]]) + invited_partners = self.env['res.partner'] + invited_guests = self.env['mail.guest'] + invitation_notifications = [] + for member in self.env['mail.channel.partner'].search(channel_partner_domain): + member.rtc_inviting_session_id = False if member.partner_id: - invited_partners.append({ - 'id': member.partner_id.id, - 'name': member.partner_id.name, - }) - elif member.guest_id: - invited_guests.append({ - 'id': member.guest_id.id, - 'name': member.guest_id.name, - }) - model, record_id = ('mail.guest', member.guest_id.id) if member.guest_id else ('res.partner', member.partner_id.id) - notifications.append([ - (self._cr.dbname, model, record_id), - { - 'type': 'rtc_incoming_invitation_update', - 'payload': { - 'channelId': self.id, - 'rtcSession': current_rtc_session._mail_rtc_session_format(), - }, - }, - ]) - notification = _("%s started a live conference", partner.name or guest.name) - self.message_post(body=notification, message_type='notification') - self.env['bus.bus'].sendmany(notifications) - return invited_partners, invited_guests - - def _join_call(self): - self.ensure_one() - session_data, session_id = self._update_call_participation(joining=True) - if not session_id: - return - ice_servers = self.env['mail.ice.server']._get_ice_servers() - invited_partners = [] - invited_guests = [] - if len(self.rtc_session_ids) == 1 and self.channel_type in {'chat', 'group'}: - invited_partners, invited_guests = self._invite_members_to_rtc() - return { - 'rtcSessions': session_data, - 'iceServers': ice_servers or False, - 'invitedGuests': invited_guests, - 'invitedPartners': invited_partners, - 'sessionId': session_id, - } - - def _leave_call(self): - self.ensure_one() - self._update_call_participation(joining=False) - - def _update_call_participation(self, joining=True): - """ Updates the call participation of the current partner and notifies members of - the channel if necessary. - :param bool joining : true if joining the call, false if leaving. - """ - if self.env.user._is_public(): - guest = self.env.context.get('guest') - partner = self.env['res.partner'] - else: - guest = self.env['mail.guest'] - partner = self.env.user.partner_id - new_session_id = None - current_channel_partner = self.env['mail.channel.partner'].search([('channel_id', '=', self.id), ('partner_id', '=', partner.id), ('guest_id', '=', guest.id)], limit=1) - if not current_channel_partner: - return - current_channel_partner._remove_rtc_invitation() - old_sessions = self.rtc_session_ids.filtered(lambda s: s.channel_partner_id == current_channel_partner) - old_sessions._disconnect() - if joining: - new_session = self.env['mail.channel.rtc.session'].create({'channel_partner_id': current_channel_partner.id}) - new_session_id = new_session.id - elif not old_sessions: - return - - session_data_by_channel = self._notify_rtc_sessions_change() - return session_data_by_channel.get(self.id, []), new_session_id - - def _notify_rtc_sessions_change(self): - session_data_by_channel = self.rtc_session_ids._mail_rtc_session_format_by_channel() - notifications = [] - for record in self: - sessions_data = session_data_by_channel.get(record.id, []) - if not sessions_data: - # if there is no member left in the rtc call, all invitations are reset - record._cancel_rtc_invitations() - notifications.append([(self._cr.dbname, 'mail.channel', record.id), { - 'type': 'rtc_sessions_update', + invited_partners |= member.partner_id + target = (self._cr.dbname, 'res.partner', member.partner_id.id) + else: + invited_guests |= member.guest_id + target = (self._cr.dbname, 'mail.guest', member.guest_id.id) + invitation_notifications.append((target, { + 'type': 'mail.channel_update', 'payload': { - 'channelId': record.id, - 'rtcSessions': sessions_data, + 'id': self.id, + 'rtcInvitingSession': [('unlink',)], }, - }]) - self.env['bus.bus'].sendmany(notifications) - return session_data_by_channel + })) + self.env['bus.bus'].sendmany(invitation_notifications) + channel_data = {'id': self.id} + if invited_guests: + channel_data['invitedGuests'] = [('insert-and-unlink', [{'id': guest.id} for guest in invited_guests])] + if invited_partners: + channel_data['invitedPartners'] = [('insert-and-unlink', [{'id': partner.id} for partner in invited_partners])] + if invited_partners or invited_guests: + self.env['bus.bus'].sendone((self._cr.dbname, 'mail.channel', self.id), { + 'type': 'mail.channel_update', + 'payload': channel_data, + }) + return channel_data # ------------------------------------------------------------ # MAILING @@ -660,9 +556,9 @@ class Channel(models.Model): self.env['bus.bus'].sudo().sendmany(bus_notifications) # Last interest is updated for a chat when posting a message. # So a notification is needed to update UI. - if self.is_chat: + if self.is_chat or self.channel_type == 'group': notifications = [] - for channel_partners in self.channel_last_seen_partner_ids: + for channel_partners in self.channel_last_seen_partner_ids.filtered('partner_id'): notif = { 'type': 'mail.channel_last_interest_dt_changed', 'payload': { @@ -873,7 +769,7 @@ class Channel(models.Model): info['last_interest_dt'] = partner_channel.last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT) if partner_channel.rtc_inviting_session_id: info['rtc_inviting_session'] = {'id': partner_channel.rtc_inviting_session_id.id} - # add members infos + # add members info if channel.channel_type != 'channel': # avoid sending potentially a lot of members for big channels # exclude chat and other small channels from this optimization because they are @@ -886,9 +782,12 @@ class Channel(models.Model): 'seen_message_id': cp.seen_message_id.id, } for cp in channel_partners], key=lambda p: p['partner_id']) - # add rtc sessions infos - if rtc_sessions_by_channel.get(channel.id): - info['rtc_sessions'] = rtc_sessions_by_channel[channel.id] + # add RTC sessions info + info.update({ + 'invitedGuests': [('insert-and-replace', [{'id': guest.id, 'name': guest.name} for guest in channel_partners.filtered('rtc_inviting_session_id').guest_id])], + 'invitedPartners': [('insert-and-replace', [{'id': partner.id, 'name': partner.name} for partner in channel_partners.filtered('rtc_inviting_session_id').partner_id])], + 'rtcSessions': [('insert-and-replace', rtc_sessions_by_channel.get(channel, []))], + }) channel_infos.append(info) return channel_infos diff --git a/addons/mail/models/mail_channel_partner.py b/addons/mail/models/mail_channel_partner.py index 778da1a6485..83462ef188f 100644 --- a/addons/mail/models/mail_channel_partner.py +++ b/addons/mail/models/mail_channel_partner.py @@ -5,6 +5,7 @@ from werkzeug.exceptions import NotFound from odoo import api, fields, models, _ from odoo.exceptions import AccessError +from odoo.osv import expression class ChannelPartner(models.Model): @@ -26,34 +27,13 @@ class ChannelPartner(models.Model): is_minimized = fields.Boolean("Conversation is minimized") is_pinned = fields.Boolean("Is pinned on the interface", default=True) last_interest_dt = fields.Datetime("Last Interest", default=fields.Datetime.now, help="Contains the date and time of the last interesting event that happened in this channel for this partner. This includes: creating, joining, pinning, and new message posted.") + # RTC + rtc_session_ids = fields.One2many(string="RTC Sessions", comodel_name='mail.channel.rtc.session', inverse_name='channel_partner_id') rtc_inviting_session_id = fields.Many2one('mail.channel.rtc.session', string='Ringing session') def name_get(self): return [(record.id, record.partner_id.name or record.guest_id.name) for record in self] - def _remove_rtc_invitation(self): - """ Removes the invitation to the rtc call and notifies the inviting partner if removed. """ - notifications = [] - for record in self: - if not record.rtc_inviting_session_id: - continue - model, record_id = ('mail.guest', record.rtc_inviting_session_id.guest_id.id) if record.rtc_inviting_session_id.guest_id else ( - 'res.partner', record.rtc_inviting_session_id.partner_id.id) - payload = {'channelId': record.channel_id.id} - if record.partner_id: - payload['partnerId'] = record.partner_id.id - else: - payload['guestId'] = record.guest_id.id - notifications.append([ - (self._cr.dbname, model, record_id), - { - 'type': 'rtc_outgoing_invitation_ended', - 'payload': payload, - }, - ]) - self.write({'rtc_inviting_session_id': False}) - self.env['bus.bus'].sendmany(notifications) - def init(self): self.env.cr.execute("CREATE UNIQUE INDEX IF NOT EXISTS mail_channel_partner_partner_unique ON %s (channel_id, partner_id) WHERE partner_id IS NOT NULL" % self._table) self.env.cr.execute("CREATE UNIQUE INDEX IF NOT EXISTS mail_channel_partner_guest_unique ON %s (channel_id, guest_id) WHERE guest_id IS NOT NULL" % self._table) @@ -111,3 +91,83 @@ class ChannelPartner(models.Model): if guest: return guest.env['mail.channel.partner'].sudo().search([('channel_id', '=', channel_id), ('guest_id', '=', guest.id)], limit=1) return self.env['mail.channel.partner'].sudo() + + # -------------------------------------------------------------------------- + # RTC (voice/video) + # -------------------------------------------------------------------------- + + def _rtc_join_call(self): + self.ensure_one() + self.channel_id._rtc_cancel_invitations(partner_ids=self.partner_id.ids, guest_ids=self.guest_id.ids) + self.rtc_session_ids._disconnect() + rtc_session = self.env['mail.channel.rtc.session'].create({'channel_partner_id': self.id}) + res = { + 'iceServers': self.env['mail.ice.server']._get_ice_servers() or False, + 'rtcSessions': [('insert-and-replace', self.channel_id.rtc_session_ids._mail_rtc_session_format_by_channel().get(self.channel_id))], + 'sessionId': rtc_session.id, + } + if len(self.channel_id.rtc_session_ids) == 1 and self.channel_id.channel_type in {'chat', 'group'}: + self.channel_id.message_post(body=_("%s started a live conference", self.partner_id.name or self.guest_id.name), message_type='notification') + invited_partners, invited_guests = self._rtc_invite_members() + if invited_guests: + res['invitedGuests'] = [('insert', [{'id': guest.id, 'name': guest.name} for guest in invited_guests])] + if invited_partners: + res['invitedPartners'] = [('insert', [{'id': partner.id, 'name': partner.name} for partner in invited_partners])] + return res + + def _rtc_leave_call(self): + self.ensure_one() + if self.rtc_session_ids: + self.rtc_session_ids.unlink() + else: + return self.channel_id._rtc_cancel_invitations(partner_ids=self.partner_id.ids, guest_ids=self.guest_id.ids) + + def _rtc_invite_members(self, partner_ids=None, guest_ids=None): + """ Sends invitations to join the RTC call to all connected members of the thread who are not already invited. + :param list partner_ids: list of the partner ids to invite + :param list guest_ids: list of the guest ids to invite + + if either partner_ids or guest_ids is set, only the specified ids will be invited. + """ + self.ensure_one() + channel_partner_domain = [ + ('channel_id', '=', self.channel_id.id), + ('rtc_inviting_session_id', '=', False), + ('rtc_session_ids', '=', False), + ] + if partner_ids or guest_ids: + channel_partner_domain = expression.AND([channel_partner_domain, [ + '|', + ('partner_id', 'in', partner_ids or []), + ('guest_id', 'in', guest_ids or []), + ]]) + invitation_notifications = [] + invited_partners = self.env['res.partner'] + invited_guests = self.env['mail.guest'] + for member in self.env['mail.channel.partner'].search(channel_partner_domain): + member.rtc_inviting_session_id = self.rtc_session_ids.id + if member.partner_id: + invited_partners |= member.partner_id + target = (self._cr.dbname, 'res.partner', member.partner_id.id) + else: + invited_guests |= member.guest_id + target = (self._cr.dbname, 'mail.guest', member.guest_id.id) + invitation_notifications.append((target, { + 'type': 'mail.channel_update', + 'payload': { + 'id': self.channel_id.id, + 'rtcInvitingSession': [('insert', self.rtc_session_ids._mail_rtc_session_format())], + }, + })) + self.env['bus.bus'].sendmany(invitation_notifications) + if invited_guests or invited_partners: + channel_data = {'id': self.channel_id.id} + if invited_guests: + channel_data['invitedGuests'] = [('insert', [{'id': guest.id, 'name': guest.name} for guest in invited_guests])] + if invited_partners: + channel_data['invitedPartners'] = [('insert', [{'id': partner.id, 'name': partner.name} for partner in invited_partners])] + self.env['bus.bus'].sendone((self._cr.dbname, 'mail.channel', self.channel_id.id), { + 'type': 'mail.channel_update', + 'payload': channel_data, + }) + return invited_partners, invited_guests diff --git a/addons/mail/models/mail_channel_rtc_session.py b/addons/mail/models/mail_channel_rtc_session.py index faf7f9b6639..0f84ca98a71 100644 --- a/addons/mail/models/mail_channel_rtc_session.py +++ b/addons/mail/models/mail_channel_rtc_session.py @@ -27,6 +27,35 @@ class MailRtcSession(models.Model): 'There can only be one rtc session per channel partner') ] + @api.model_create_multi + def create(self, vals_list): + rtc_sessions = super().create(vals_list) + self.env['bus.bus'].sendmany([((self._cr.dbname, 'mail.channel', channel.id), { + 'type': 'rtc_sessions_update', + 'payload': { + 'id': channel.id, + 'rtcSessions': [('insert', sessions_data)], + }, + }) for channel, sessions_data in rtc_sessions._mail_rtc_session_format_by_channel().items()]) + return rtc_sessions + + def unlink(self): + channels = self.channel_id + for channel in channels: + if channel.rtc_session_ids and len(channel.rtc_session_ids - self) == 0: + # If there is no member left in the RTC call, all invitations are cancelled. + # Note: invitation depends on field `rtc_inviting_session_id` so the cancel must be + # done before the delete to be able to know who was invited. + channel._rtc_cancel_invitations() + self.env['bus.bus'].sendmany([((self._cr.dbname, 'mail.channel', channel.id), { + 'type': 'rtc_sessions_update', + 'payload': { + 'id': channel.id, + 'rtcSessions': [('insert-and-unlink', [{'id': session_data['id']} for session_data in sessions_data])], + }, + }) for channel, sessions_data in self._mail_rtc_session_format_by_channel().items()]) + return super().unlink() + def _update_and_broadcast(self, values): """ Updates the session and notifies all members of the channel of the change. @@ -35,10 +64,8 @@ class MailRtcSession(models.Model): self.write({key: values[key] for key in valid_values if key in valid_values}) session_data = self._mail_rtc_session_format() self.env['bus.bus'].sendone((self._cr.dbname, 'mail.channel', self.channel_id.id), { - 'type': 'rtc_session_data_update', - 'payload': { - 'rtcSession': session_data, - }, + 'type': 'mail.rtc_session_update', + 'payload': session_data, }) @api.autovacuum @@ -47,34 +74,25 @@ class MailRtcSession(models.Model): this can happen when the server or the user's browser crash or when the user's odoo session ends. """ - sessions = self.search([ - ('write_date', '<', fields.Datetime.now() - relativedelta(days=1)) - ]) - if not sessions: - return - channel_ids = sessions.channel_id - sessions.unlink() - channel_ids._notify_rtc_sessions_change() + rtc_sessions = self.search([('write_date', '<', fields.Datetime.now() - relativedelta(days=1))]) + rtc_sessions._disconnect() def action_disconnect(self): - channels = self.channel_id self._disconnect() - if channels: - channels._notify_rtc_sessions_change() def _disconnect(self): """ Unlinks the sessions and notifies the associated partners/guests that their session ended. """ notifications = [] - for record in self: - model, record_id = ('mail.guest', record.guest_id.id) if record.guest_id else ('res.partner', record.partner_id.id) + for rtc_session in self: + model_name, record_id = ('mail.guest', rtc_session.guest_id.id) if rtc_session.guest_id else ('res.partner', rtc_session.partner_id.id) notifications.append([ - (self._cr.dbname, model, record_id), + (self._cr.dbname, model_name, record_id), { 'type': 'rtc_session_ended', 'payload': { - 'sessionId': record.id, + 'sessionId': rtc_session.id, }, }, ]) @@ -105,27 +123,28 @@ class MailRtcSession(models.Model): return self.env['bus.bus'].sendmany(notifications) def _mail_rtc_session_format(self): + self.ensure_one() vals = { 'id': self.id, - 'is_screen_sharing_on': self.is_screen_sharing_on, - 'is_muted': self.is_muted, - 'is_deaf': self.is_deaf, - 'is_camera_on': self.is_camera_on, + 'isCameraOn': self.is_camera_on, + 'isDeaf': self.is_deaf, + 'isMuted': self.is_muted, + 'isScreenSharingOn': self.is_screen_sharing_on, } if self.guest_id: - vals['guest'] = { + vals['guest'] = [('insert', { 'id': self.guest_id.id, 'name': self.guest_id.name, - } + })] else: - vals['partner'] = { + vals['partner'] = [('insert', { 'id': self.partner_id.id, 'name': self.partner_id.name, - } + })] return vals def _mail_rtc_session_format_by_channel(self): data = {} - for record in self: - data.setdefault(record.channel_id.id, []).append(record._mail_rtc_session_format()) + for rtc_session in self: + data.setdefault(rtc_session.channel_id, []).append(rtc_session._mail_rtc_session_format()) return data diff --git a/addons/mail/static/src/model/model_manager.js b/addons/mail/static/src/model/model_manager.js index 750828fc4c0..e3a890d221a 100644 --- a/addons/mail/static/src/model/model_manager.js +++ b/addons/mail/static/src/model/model_manager.js @@ -335,13 +335,14 @@ export class ModelManager { * @param {Listener} listener */ removeListener(listener) { + this._listeners.delete(listener); this._listenersToNotifyInUpdateCycle.delete(listener); this._listenersToNotifyAfterUpdateCycle.delete(listener); for (const localId of this._localIdsObservedByListener.get(listener) || []) { this._listenersObservingLocalId.get(localId).delete(listener); if (this._listenersObservingFieldOfLocalId.has(localId)) { - for (const [, componentsUsingField] of this._listenersObservingFieldOfLocalId.get(localId)) { - componentsUsingField.delete(listener); + for (const [, listenersUsingField] of this._listenersObservingFieldOfLocalId.get(localId)) { + listenersUsingField.delete(listener); } } } @@ -794,7 +795,6 @@ export class ModelManager { this._createdRecords.delete(record); for (const listener of this._listenersObservingLocalId.get(record.localId)) { this._markListenerToNotify(listener); - this._localIdsObservedByListener.get(listener).delete(record.localId); } for (const listener of this._listenersObservingAllByModel.get(Model)) { this._markListenerToNotify(listener); diff --git a/addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js b/addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js index 21260432dcc..56ce19219ba 100644 --- a/addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js +++ b/addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js @@ -93,20 +93,18 @@ function factory(dependencies) { return this.messaging.models['mail.guest'].insert(message.payload); case 'mail.message_update': return this.messaging.models['mail.message'].insert(message.payload); + case 'mail.rtc_session_update': + return this.messaging.models['mail.rtc_session'].insert(message.payload); case 'res.users_settings_changed': return this._handleNotificationResUsersSettings(message.payload); case 'rtc_peer_notification': return this._handleNotificationRtcPeerToPeer(message.payload); case 'rtc_incoming_invitation_update': return this._handleNotificationRtcInvitation(message.payload); - case 'rtc_outgoing_invitation_ended': - return this._handleNotificationRtcInvitationEnded(message.payload); case 'rtc_sessions_update': return this._handleNotificationRtcSessionUpdate(message.payload); case 'rtc_session_ended': return this._handleNotificationRtcSessionEnded(message.payload); - case 'rtc_session_data_update': - return this._handleNotificationRtcSessionDataUpdate(message.payload); case 'res_users_settings_volumes_update': return this._handleNotificationVolumeSettingUpdate(message.payload); } @@ -545,25 +543,6 @@ function factory(dependencies) { return channel.update({ rtcInvitingSession: unlink() }); } - /** - * @private - * @param {Object} data - * @param {number} [data.channelId] - * @param {number} [data.partnerId] - */ - async _handleNotificationRtcInvitationEnded({ channelId, partnerId, guestId }) { - const channel = this.messaging.models['mail.thread'].findFromIdentifyingData({ id: channelId, model: 'mail.channel' }); - if (!channel) { - return; - } - const partner = partnerId && this.messaging.models['mail.partner'].findFromIdentifyingData({ id: partnerId }); - const guest = guestId && this.messaging.models['mail.guest'].findFromIdentifyingData({ id: guestId }); - channel.update({ - invitedPartners: partner && unlink(partner), - invitedGuests: guest && unlink(guest), - }); - } - /** * @private * @param {Object} data @@ -574,16 +553,6 @@ function factory(dependencies) { this.messaging.mailRtc.handleNotification(sender, content); } - /** - * @private - * @param {Object} data - * @param {Object} [data.rtcSession] - */ - async _handleNotificationRtcSessionDataUpdate({ rtcSession }) { - const rtcSessionModel = this.messaging.models['mail.rtc_session']; - rtcSessionModel.insert(rtcSessionModel.convertData(rtcSession)); - } - /** * @private * @param {Object} data @@ -614,11 +583,11 @@ function factory(dependencies) { /** * @private * @param {Object} data - * @param {string} [data.channelId] + * @param {string} [data.id] * @param {Object} [data.rtcSessions] */ - async _handleNotificationRtcSessionUpdate({ channelId, rtcSessions }) { - const channel = this.messaging.models['mail.thread'].findFromIdentifyingData({ id: channelId, model: 'mail.channel' }); + async _handleNotificationRtcSessionUpdate({ id, rtcSessions }) { + const channel = this.messaging.models['mail.thread'].findFromIdentifyingData({ id, model: 'mail.channel' }); if (!channel) { return; } diff --git a/addons/mail/static/src/models/rtc/rtc.js b/addons/mail/static/src/models/rtc/rtc.js index 146dc25e1d8..017605de6f8 100644 --- a/addons/mail/static/src/models/rtc/rtc.js +++ b/addons/mail/static/src/models/rtc/rtc.js @@ -101,17 +101,16 @@ function factory(dependencies) { */ async filterCallees(currentSessions) { const currentSessionsTokens = new Set(currentSessions.map(session => session.peerToken)); - if (this.currentRtcSession && !currentSessionsTokens.has(this.currentRtcSession.peerToken)) { - // if the current RTC session is not in the channel sessions, this call is no longer valid. - this.channel && this.channel.endCall(); - return; - } for (const token of Object.keys(this._peerConnections)) { if (!currentSessionsTokens.has(token)) { this._addLogEntry(token, 'session removed from the server'); this._removePeer(token); } } + if (this.channel && this.currentRtcSession && !currentSessionsTokens.has(this.currentRtcSession.peerToken)) { + // if the current RTC session is not in the channel sessions, this call is no longer valid. + this.channel.endCall(); + } } /** diff --git a/addons/mail/static/src/models/rtc_call_participant_card/rtc_call_participant_card.js b/addons/mail/static/src/models/rtc_call_participant_card/rtc_call_participant_card.js index e8e2dbcf77d..2872bfdc84c 100644 --- a/addons/mail/static/src/models/rtc_call_participant_card/rtc_call_participant_card.js +++ b/addons/mail/static/src/models/rtc_call_participant_card/rtc_call_participant_card.js @@ -2,7 +2,6 @@ import { registerNewModel } from '@mail/model/model_core'; import { attr, many2one, one2one } from '@mail/model/model_field'; -import { unlink } from '@mail/model/model_field_command'; function factory(dependencies) { @@ -33,10 +32,11 @@ function factory(dependencies) { * @param {MouseEvent} ev */ async onClick(ev) { - if (!this.invitedPartner || this.invitedGuest) { + if (!this.invitedPartner && !this.invitedGuest) { return; } - await this.env.services.rpc(({ + const channel = this.channel; + const channelData = await this.env.services.rpc(({ route: '/mail/rtc/channel/cancel_call_invitation', params: { channel_id: this.channel.id, @@ -44,10 +44,10 @@ function factory(dependencies) { guest_ids: this.invitedGuest && [this.invitedGuest.id], }, })); - this.channel.update({ - invitedGuests: this.invitedGuest && unlink(this.invitedGuest), - invitedPartners: this.invitedPartner && unlink(this.invitedPartner), - }); + if (!channel.exists()) { + return; + } + channel.update(channelData); } /** diff --git a/addons/mail/static/src/models/rtc_session/rtc_session.js b/addons/mail/static/src/models/rtc_session/rtc_session.js index 8ca7e95fb10..3b42a79b8d2 100644 --- a/addons/mail/static/src/models/rtc_session/rtc_session.js +++ b/addons/mail/static/src/models/rtc_session/rtc_session.js @@ -31,42 +31,6 @@ function factory(dependencies) { // Public //---------------------------------------------------------------------- - /** - * @static - * @param {Object} data - * @return {Object} - */ - static convertData(data) { - const data2 = {}; - if ('id' in data) { - data2.id = data.id; - } - if ('is_camera_on' in data) { - data2.isCameraOn = data.is_camera_on; - } - if ('is_deaf' in data) { - data2.isDeaf = data.is_deaf; - } - if ('is_muted' in data) { - data2.isMuted = data.is_muted; - } - if ('is_screen_sharing_on' in data) { - data2.isScreenSharingOn = data.is_screen_sharing_on; - } - - // relations - if ('partner' in data) { - data2.partner = insert(data.partner); - } - if ('guest' in data) { - data2.guest = insert(data.guest); - } - if ('channel' in data) { - data2.channel = insert(data.channel); - } - return data2; - } - /** * restores the session to its default values */ diff --git a/addons/mail/static/src/models/thread/thread.js b/addons/mail/static/src/models/thread/thread.js index 135c1026427..421d94e9ea3 100644 --- a/addons/mail/static/src/models/thread/thread.js +++ b/addons/mail/static/src/models/thread/thread.js @@ -200,6 +200,12 @@ function factory(dependencies) { if ('id' in data) { data2.id = data.id; } + if ('invitedGuests' in data) { + data2.invitedGuests = data.invitedGuests; + } + if ('invitedPartners' in data) { + data2.invitedPartners = data.invitedPartners; + } if ('is_minimized' in data && 'state' in data) { data2.serverFoldState = data.is_minimized ? data.state : 'closed'; } @@ -261,8 +267,8 @@ function factory(dependencies) { if ('rtc_inviting_session' in data) { data2.rtcInvitingSession = insert(data.rtc_inviting_session); } - if ('rtc_sessions' in data) { - data2.rtcSessions = insert(data.rtc_sessions.map(record => this.messaging.models['mail.rtc_session'].convertData(record))); + if ('rtcSessions' in data) { + data2.rtcSessions = data.rtcSessions; } if ('seen_partners_info' in data) { if (!data.seen_partners_info) { @@ -325,8 +331,6 @@ function factory(dependencies) { this.messaging.soundEffects.channelLeave.play(); } this.update({ - invitedGuests: clear(), - invitedPartners: clear(), mailRtc: unlink(), rtcInvitingSession: unlink(), }); @@ -776,12 +780,15 @@ function factory(dependencies) { channel_id: this.id, }, }, { shadow: true })); + if (!this.exists()) { + return; + } this.update({ mailRtc: link(this.messaging.mailRtc), rtcInvitingSession: unlink(), - rtcSessions: insertAndReplace(rtcSessions.map(record => this.messaging.models['mail.rtc_session'].convertData(record))), - invitedGuests: insertAndReplace(invitedGuests), - invitedPartners: insertAndReplace(invitedPartners), + rtcSessions, + invitedGuests, + invitedPartners, }); await this.async(() => this.messaging.mailRtc.initSession({ currentSessionId: sessionId, @@ -806,9 +813,7 @@ function factory(dependencies) { */ updateRtcSessions(rtcSessions) { const oldCount = this.rtcSessions.length; - this.update({ - rtcSessions: insertAndReplace(rtcSessions.map(record => this.messaging.models['mail.rtc_session'].convertData(record))) - }); + this.update({ rtcSessions }); if (this.mailRtc) { const newCount = this.rtcSessions.length; if (newCount > oldCount) { diff --git a/addons/mail/tests/__init__.py b/addons/mail/tests/__init__.py index 4f925a5e71b..e1c33cc3fdf 100644 --- a/addons/mail/tests/__init__.py +++ b/addons/mail/tests/__init__.py @@ -10,4 +10,5 @@ from . import test_mail_template from . import test_mail_tools from . import test_res_partner from . import test_res_users_settings +from . import test_rtc from . import test_update_notification diff --git a/addons/mail/tests/test_rtc.py b/addons/mail/tests/test_rtc.py new file mode 100644 index 00000000000..590499d878e --- /dev/null +++ b/addons/mail/tests/test_rtc.py @@ -0,0 +1,574 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from dateutil.relativedelta import relativedelta + +from odoo import fields +from odoo.addons.mail.tests.common import MailCommon +from odoo.tests import tagged +from odoo.tests.common import users +from odoo.tools import mute_logger + + +@tagged('RTC') +class TestChannelInternals(MailCommon): + + @users('employee') + @mute_logger('odoo.models.unlink') + def test_01_join_call(self): + """Join call should remove existing sessions, remove invitation, create a new session, and return data.""" + channel = self.env['mail.channel'].browse(self.env['mail.channel'].channel_create(name='Test Channel')['id']) + channel_partner = channel.sudo().channel_last_seen_partner_ids.filtered(lambda channel_partner: channel_partner.partner_id == self.user_employee.partner_id) + channel_partner._rtc_join_call() + self.env['bus.bus'].sudo().search([]).unlink() + with self.assertBus( + [ + (self.cr.dbname, 'res.partner', self.user_employee.partner_id.id), # end of previous session + (self.cr.dbname, 'mail.channel', channel.id), # update sessions + (self.cr.dbname, 'mail.channel', channel.id), # update sessions + ], + [ + { + 'type': 'rtc_session_ended', + 'payload': { + 'sessionId': channel_partner.rtc_session_ids.id, + }, + }, + { + 'type': 'rtc_sessions_update', + 'payload': { + 'id': channel.id, + 'rtcSessions': [('insert-and-unlink', [{'id': channel_partner.rtc_session_ids.id}])], + }, + }, + { + 'type': 'rtc_sessions_update', + 'payload': { + 'id': channel.id, + 'rtcSessions': [('insert', [{ + 'id': channel_partner.rtc_session_ids.id + 1, + 'isCameraOn': False, + 'isDeaf': False, + 'isMuted': False, + 'isScreenSharingOn': False, + 'partner': [('insert', { + 'id': self.user_employee.partner_id.id, + 'name': "Ernest Employee", + })], + }])], + }, + }, + ] + ): + res = channel_partner._rtc_join_call() + self.assertEqual(res, { + 'iceServers': False, + 'rtcSessions': [('insert-and-replace', [{ + 'id': channel_partner.rtc_session_ids.id, + 'isCameraOn': False, + 'isDeaf': False, + 'isMuted': False, + 'isScreenSharingOn': False, + 'partner': [('insert', { + 'id': self.user_employee.partner_id.id, + 'name': "Ernest Employee", + })], + }])], + 'sessionId': channel_partner.rtc_session_ids.id, + }) + + @users('employee') + @mute_logger('odoo.models.unlink') + def test_10_start_call_in_chat_should_invite_all_members_to_call(self): + test_user = self.env['res.users'].sudo().create({'name': "Test User", 'login': 'test'}) + channel = self.env['mail.channel'].browse(self.env['mail.channel'].channel_get(partners_to=(self.user_employee.partner_id + test_user.partner_id).ids)['id']) + channel_partner = channel.sudo().channel_last_seen_partner_ids.filtered(lambda channel_partner: channel_partner.partner_id == self.user_employee.partner_id) + channel_partner._rtc_join_call() + last_rtc_session_id = channel_partner.rtc_session_ids.id + channel_partner._rtc_leave_call() + + self.env['bus.bus'].sudo().search([]).unlink() + with self.assertBus( + [ + (self.cr.dbname, 'mail.channel', channel.id), # update new session + (self.cr.dbname, 'mail.channel', channel.id), # message_post "started a live conference" (not asserted below) + (self.cr.dbname, 'res.partner', self.user_employee.partner_id.id), # update of last interest (not asserted below) + (self.cr.dbname, 'res.partner', test_user.partner_id.id), # update of last interest (not asserted below) + (self.cr.dbname, 'res.partner', test_user.partner_id.id), # incoming invitation + (self.cr.dbname, 'mail.channel', channel.id), # update list of invitations + ], + [ + { + 'type': 'rtc_sessions_update', + 'payload': { + 'id': channel.id, + 'rtcSessions': [('insert', [{ + 'id': last_rtc_session_id + 1, + 'isCameraOn': False, + 'isDeaf': False, + 'isMuted': False, + 'isScreenSharingOn': False, + 'partner': [('insert', { + 'id': self.user_employee.partner_id.id, + 'name': "Ernest Employee", + })], + }])], + }, + }, + { + 'type': 'mail.channel_update', + 'payload': { + 'id': channel.id, + 'invitedPartners': [('insert', [{'id': test_user.partner_id.id, 'name': 'Test User'}])], + }, + }, + ] + ): + res = channel_partner._rtc_join_call() + self.assertNotIn('invitedGuests', res) + self.assertIn('invitedPartners', res) + self.assertEqual(res['invitedPartners'], [('insert', [{'id': test_user.partner_id.id, 'name': "Test User"}])]) + + @users('employee') + @mute_logger('odoo.models.unlink') + def test_11_start_call_in_group_should_invite_all_members_to_call(self): + test_user = self.env['res.users'].sudo().create({'name': "Test User", 'login': 'test'}) + test_guest = self.env['mail.guest'].sudo().create({'name': "Test Guest"}) + channel = self.env['mail.channel'].browse(self.env['mail.channel'].create_group(partners_to=(self.user_employee.partner_id + test_user.partner_id).ids)['id']) + channel.add_members(guest_ids=test_guest.ids) + channel_partner = channel.sudo().channel_last_seen_partner_ids.filtered(lambda channel_partner: channel_partner.partner_id == self.user_employee.partner_id) + channel_partner._rtc_join_call() + last_rtc_session_id = channel_partner.rtc_session_ids.id + channel_partner._rtc_leave_call() + + self.env['bus.bus'].sudo().search([]).unlink() + with self.assertBus( + [ + (self.cr.dbname, 'mail.channel', channel.id), # update new session + (self.cr.dbname, 'mail.channel', channel.id), # message_post "started a live conference" (not asserted below) + (self.cr.dbname, 'res.partner', self.user_employee.partner_id.id), # update of last interest (not asserted below) + (self.cr.dbname, 'res.partner', test_user.partner_id.id), # update of last interest (not asserted below) + (self.cr.dbname, 'res.partner', test_user.partner_id.id), # incoming invitation + (self.cr.dbname, 'mail.guest', test_guest.id), # incoming invitation + (self.cr.dbname, 'mail.channel', channel.id), # update list of invitations + ], + [ + { + 'type': 'rtc_sessions_update', + 'payload': { + 'id': channel.id, + 'rtcSessions': [('insert', [{ + 'id': last_rtc_session_id + 1, + 'isCameraOn': False, + 'isDeaf': False, + 'isMuted': False, + 'isScreenSharingOn': False, + 'partner': [('insert', { + 'id': self.user_employee.partner_id.id, + 'name': "Ernest Employee", + })], + }])], + }, + }, + { + 'type': 'rtc_sessions_update', + 'payload': { + 'id': channel.id, + 'rtcSessions': [('insert', [{ + 'id': last_rtc_session_id + 1, + 'isCameraOn': False, + 'isDeaf': False, + 'isMuted': False, + 'isScreenSharingOn': False, + 'partner': [('insert', { + 'id': self.user_employee.partner_id.id, + 'name': "Ernest Employee", + })], + }])], + }, + }, + { + 'type': 'mail.channel_update', + 'payload': { + 'id': channel.id, + 'invitedGuests': [('insert', [{'id': test_guest.id, 'name': 'Test Guest'}])], + 'invitedPartners': [('insert', [{'id': test_user.partner_id.id, 'name': 'Test User'}])], + }, + }, + ] + ): + res = channel_partner._rtc_join_call() + self.assertIn('invitedGuests', res) + self.assertEqual(res['invitedGuests'], [('insert', [{'id': test_guest.id, 'name': 'Test Guest'}])]) + self.assertIn('invitedPartners', res) + self.assertEqual(res['invitedPartners'], [('insert', [{'id': test_user.partner_id.id, 'name': "Test User"}])]) + + @users('employee') + @mute_logger('odoo.models.unlink') + def test_20_join_call_should_cancel_pending_invitations(self): + test_user = self.env['res.users'].sudo().create({'name': "Test User", 'login': 'test'}) + test_guest = self.env['mail.guest'].sudo().create({'name': "Test Guest"}) + channel = self.env['mail.channel'].browse(self.env['mail.channel'].create_group(partners_to=(self.user_employee.partner_id + test_user.partner_id).ids)['id']) + channel.add_members(guest_ids=test_guest.ids) + channel_partner = channel.sudo().channel_last_seen_partner_ids.filtered(lambda channel_partner: channel_partner.partner_id == self.user_employee.partner_id) + channel_partner._rtc_join_call() + + channel_partner_test_user = channel.sudo().channel_last_seen_partner_ids.filtered(lambda channel_partner: channel_partner.partner_id == test_user.partner_id) + self.env['bus.bus'].sudo().search([]).unlink() + with self.assertBus( + [ + (self.cr.dbname, 'res.partner', test_user.partner_id.id), # update invitation + (self.cr.dbname, 'mail.channel', channel.id), # update list of invitations + (self.cr.dbname, 'mail.channel', channel.id), # update sessions + ], + [ + { + 'type': 'mail.channel_update', + 'payload': { + 'id': channel.id, + 'rtcInvitingSession': [('unlink',)], + }, + }, + { + 'type': 'mail.channel_update', + 'payload': { + 'id': channel.id, + 'invitedPartners': [('insert-and-unlink', [{'id': test_user.partner_id.id}])], + }, + }, + { + 'type': 'rtc_sessions_update', + 'payload': { + 'id': channel.id, + 'rtcSessions': [('insert', [ + { + 'id': channel_partner.rtc_session_ids.id + 1, + 'isCameraOn': False, + 'isDeaf': False, + 'isMuted': False, + 'isScreenSharingOn': False, + 'partner': [('insert', { + 'id': test_user.partner_id.id, + 'name': 'Test User', + })], + }, + ])], + }, + }, + ] + ): + channel_partner_test_user._rtc_join_call() + + channel_partner_test_guest = channel.sudo().channel_last_seen_partner_ids.filtered(lambda channel_partner: channel_partner.guest_id == test_guest) + self.env['bus.bus'].sudo().search([]).unlink() + with self.assertBus( + [ + (self.cr.dbname, 'mail.guest', test_guest.id), # update invitation + (self.cr.dbname, 'mail.channel', channel.id), # update list of invitations + (self.cr.dbname, 'mail.channel', channel.id), # update sessions + ], + [ + { + 'type': 'mail.channel_update', + 'payload': { + 'id': channel.id, + 'rtcInvitingSession': [('unlink',)], + }, + }, + { + 'type': 'mail.channel_update', + 'payload': { + 'id': channel.id, + 'invitedGuests': [('insert-and-unlink', [{'id': test_guest.id}])], + }, + }, + { + 'type': 'rtc_sessions_update', + 'payload': { + 'id': channel.id, + 'rtcSessions': [('insert', [ + { + 'id': channel_partner.rtc_session_ids.id + 2, + 'isCameraOn': False, + 'isDeaf': False, + 'isMuted': False, + 'isScreenSharingOn': False, + 'guest': [('insert', { + 'id': test_guest.id, + 'name': 'Test Guest', + })], + }, + ])], + }, + }, + ] + ): + channel_partner_test_guest._rtc_join_call() + + @users('employee') + @mute_logger('odoo.models.unlink') + def test_21_leave_call_should_cancel_pending_invitations(self): + test_user = self.env['res.users'].sudo().create({'name': "Test User", 'login': 'test'}) + test_guest = self.env['mail.guest'].sudo().create({'name': "Test Guest"}) + channel = self.env['mail.channel'].browse(self.env['mail.channel'].create_group(partners_to=(self.user_employee.partner_id + test_user.partner_id).ids)['id']) + channel.add_members(guest_ids=test_guest.ids) + channel_partner = channel.sudo().channel_last_seen_partner_ids.filtered(lambda channel_partner: channel_partner.partner_id == self.user_employee.partner_id) + channel_partner._rtc_join_call() + + channel_partner_test_user = channel.sudo().channel_last_seen_partner_ids.filtered(lambda channel_partner: channel_partner.partner_id == test_user.partner_id) + self.env['bus.bus'].sudo().search([]).unlink() + with self.assertBus( + [ + (self.cr.dbname, 'res.partner', test_user.partner_id.id), # update invitation + (self.cr.dbname, 'mail.channel', channel.id), # update list of invitations + ], + [ + { + 'type': 'mail.channel_update', + 'payload': { + 'id': channel.id, + 'rtcInvitingSession': [('unlink',)], + }, + }, + { + 'type': 'mail.channel_update', + 'payload': { + 'id': channel.id, + 'invitedPartners': [('insert-and-unlink', [{'id': test_user.partner_id.id}])], + }, + }, + ] + ): + channel_partner_test_user._rtc_leave_call() + + channel_partner_test_guest = channel.sudo().channel_last_seen_partner_ids.filtered(lambda channel_partner: channel_partner.guest_id == test_guest) + self.env['bus.bus'].sudo().search([]).unlink() + with self.assertBus( + [ + (self.cr.dbname, 'mail.guest', test_guest.id), # update invitation + (self.cr.dbname, 'mail.channel', channel.id), # update list of invitations + ], + [ + { + 'type': 'mail.channel_update', + 'payload': { + 'id': channel.id, + 'rtcInvitingSession': [('unlink',)], + }, + }, + { + 'type': 'mail.channel_update', + 'payload': { + 'id': channel.id, + 'invitedGuests': [('insert-and-unlink', [{'id': test_guest.id}])], + }, + }, + ] + ): + channel_partner_test_guest._rtc_leave_call() + + @users('employee') + @mute_logger('odoo.models.unlink') + def test_25_lone_call_participant_leaving_call_should_cancel_pending_invitations(self): + test_user = self.env['res.users'].sudo().create({'name': "Test User", 'login': 'test'}) + test_guest = self.env['mail.guest'].sudo().create({'name': "Test Guest"}) + channel = self.env['mail.channel'].browse(self.env['mail.channel'].create_group(partners_to=(self.user_employee.partner_id + test_user.partner_id).ids)['id']) + channel.add_members(guest_ids=test_guest.ids) + channel_partner = channel.sudo().channel_last_seen_partner_ids.filtered(lambda channel_partner: channel_partner.partner_id == self.user_employee.partner_id) + channel_partner._rtc_join_call() + + self.env['bus.bus'].sudo().search([]).unlink() + with self.assertBus( + [ + (self.cr.dbname, 'res.partner', test_user.partner_id.id), # update invitation + (self.cr.dbname, 'mail.guest', test_guest.id), # update invitation + (self.cr.dbname, 'mail.channel', channel.id), # update list of invitations + (self.cr.dbname, 'mail.channel', channel.id), # update sessions + ], + [ + { + 'type': 'mail.channel_update', + 'payload': { + 'id': channel.id, + 'rtcInvitingSession': [('unlink',)], + }, + }, + { + 'type': 'mail.channel_update', + 'payload': { + 'id': channel.id, + 'rtcInvitingSession': [('unlink',)], + }, + }, + { + 'type': 'mail.channel_update', + 'payload': { + 'id': channel.id, + 'invitedGuests': [('insert-and-unlink', [{'id': test_guest.id}])], + 'invitedPartners': [('insert-and-unlink', [{'id': test_user.partner_id.id}])], + }, + }, + { + 'type': 'rtc_sessions_update', + 'payload': { + 'id': channel.id, + 'rtcSessions': [('insert-and-unlink', [{'id': channel_partner.rtc_session_ids.id}])], + }, + }, + ] + ): + channel_partner._rtc_leave_call() + + @users('employee') + @mute_logger('odoo.models.unlink') + def test_30_add_members_while_in_call_should_invite_new_members_to_call(self): + test_user = self.env['res.users'].sudo().create({'name': "Test User", 'login': 'test'}) + test_guest = self.env['mail.guest'].sudo().create({'name': "Test Guest"}) + channel = self.env['mail.channel'].browse(self.env['mail.channel'].create_group(partners_to=self.user_employee.partner_id.ids)['id']) + channel_partner = channel.sudo().channel_last_seen_partner_ids.filtered(lambda channel_partner: channel_partner.partner_id == self.user_employee.partner_id) + channel_partner._rtc_join_call() + self.env['bus.bus'].sudo().search([]).unlink() + with self.assertBus( + [ + (self.cr.dbname, 'res.partner', test_user.partner_id.id), # channel joined (not asserted below) + (self.cr.dbname, 'mail.channel', channel.id), # message_post "invited" (not asserted below) + (self.cr.dbname, 'res.partner', self.user_employee.partner_id.id), # update of last interest (not asserted below) + (self.cr.dbname, 'res.partner', test_user.partner_id.id), # update of last interest (not asserted below) + (self.cr.dbname, 'mail.channel', channel.id), # new members (not asserted below) + (self.cr.dbname, 'res.partner', test_user.partner_id.id), # incoming invitation + (self.cr.dbname, 'mail.guest', test_guest.id), # incoming invitation + (self.cr.dbname, 'mail.channel', channel.id), # update list of invitations + ], + [ + { + 'type': 'mail.channel_update', + 'payload': { + 'id': channel.id, + 'rtcInvitingSession': [('insert', { + 'id': channel_partner.rtc_session_ids.id, + 'isCameraOn': False, + 'isDeaf': False, + 'isMuted': False, + 'isScreenSharingOn': False, + 'partner': [('insert', { + 'id': self.user_employee.partner_id.id, + 'name': "Ernest Employee", + })], + })], + }, + }, + { + 'type': 'mail.channel_update', + 'payload': { + 'id': channel.id, + 'rtcInvitingSession': [('insert', { + 'id': channel_partner.rtc_session_ids.id, + 'isCameraOn': False, + 'isDeaf': False, + 'isMuted': False, + 'isScreenSharingOn': False, + 'partner': [('insert', { + 'id': self.user_employee.partner_id.id, + 'name': "Ernest Employee", + })], + })], + }, + }, + { + 'type': 'mail.channel_update', + 'payload': { + 'id': channel.id, + 'invitedGuests': [('insert', [{'id': test_guest.id, 'name': 'Test Guest'}])], + 'invitedPartners': [('insert', [{'id': test_user.partner_id.id, 'name': 'Test User'}])], + }, + }, + ], + ): + channel.add_members(partner_ids=test_user.partner_id.ids, guest_ids=test_guest.ids, invite_to_rtc_call=True) + + @users('employee') + @mute_logger('odoo.models.unlink') + def test_40_leave_call_should_remove_existing_sessions_of_user_in_channel_and_return_data(self): + channel = self.env['mail.channel'].browse(self.env['mail.channel'].create_group(partners_to=self.user_employee.partner_id.ids)['id']) + channel_partner = channel.sudo().channel_last_seen_partner_ids.filtered(lambda channel_partner: channel_partner.partner_id == self.user_employee.partner_id) + channel_partner._rtc_join_call() + self.env['bus.bus'].sudo().search([]).unlink() + with self.assertBus( + [ + (self.cr.dbname, 'mail.channel', channel.id), # update list of sessions + ], + [ + { + 'type': 'rtc_sessions_update', + 'payload': { + 'id': channel.id, + 'rtcSessions': [('insert-and-unlink', [{'id': channel_partner.rtc_session_ids.id}])], + }, + }, + ], + ): + channel_partner._rtc_leave_call() + + @users('employee') + @mute_logger('odoo.models.unlink') + def test_50_garbage_collect_should_remove_old_sessions_and_notify_data(self): + channel = self.env['mail.channel'].browse(self.env['mail.channel'].create_group(partners_to=self.user_employee.partner_id.ids)['id']) + channel_partner = channel.sudo().channel_last_seen_partner_ids.filtered(lambda channel_partner: channel_partner.partner_id == self.user_employee.partner_id) + channel_partner._rtc_join_call() + channel_partner.rtc_session_ids.flush() + channel_partner.rtc_session_ids._write({'write_date': fields.Datetime.now() - relativedelta(days=2)}) + self.env['bus.bus'].sudo().search([]).unlink() + with self.assertBus( + [ + (self.cr.dbname, 'res.partner', self.user_employee.partner_id.id), # session ended + (self.cr.dbname, 'mail.channel', channel.id), # update list of sessions + ], + [ + { + 'type': 'rtc_session_ended', + 'payload': { + 'sessionId': channel_partner.rtc_session_ids.id, + }, + }, + { + 'type': 'rtc_sessions_update', + 'payload': { + 'id': channel.id, + 'rtcSessions': [('insert-and-unlink', [{'id': channel_partner.rtc_session_ids.id}])], + }, + }, + ], + ): + self.env['mail.channel.rtc.session'].sudo()._gc_inactive_sessions() + self.assertFalse(channel_partner.rtc_session_ids) + + @users('employee') + @mute_logger('odoo.models.unlink') + def test_51_action_disconnect_should_remove_selected_session_and_notify_data(self): + channel = self.env['mail.channel'].browse(self.env['mail.channel'].create_group(partners_to=self.user_employee.partner_id.ids)['id']) + channel_partner = channel.sudo().channel_last_seen_partner_ids.filtered(lambda channel_partner: channel_partner.partner_id == self.user_employee.partner_id) + channel_partner._rtc_join_call() + self.env['bus.bus'].sudo().search([]).unlink() + with self.assertBus( + [ + (self.cr.dbname, 'res.partner', self.user_employee.partner_id.id), # session ended + (self.cr.dbname, 'mail.channel', channel.id), # update list of sessions + ], + [ + { + 'type': 'rtc_session_ended', + 'payload': { + 'sessionId': channel_partner.rtc_session_ids.id, + }, + }, + { + 'type': 'rtc_sessions_update', + 'payload': { + 'id': channel.id, + 'rtcSessions': [('insert-and-unlink', [{'id': channel_partner.rtc_session_ids.id}])], + }, + }, + ], + ): + channel_partner.rtc_session_ids.action_disconnect() + self.assertFalse(channel_partner.rtc_session_ids) diff --git a/addons/test_discuss_full/tests/test_performance.py b/addons/test_discuss_full/tests/test_performance.py index 5ddd1396d19..32412e36325 100644 --- a/addons/test_discuss_full/tests/test_performance.py +++ b/addons/test_discuss_full/tests/test_performance.py @@ -115,6 +115,8 @@ class TestDiscussFullPerformance(TransactionCase): 'description': 'General announcements for all employees.', 'group_based_subscription': True, 'id': channel_general.id, + 'invitedGuests': [('insert-and-replace', [])], + 'invitedPartners': [('insert-and-replace', [])], 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': channel_general.channel_last_seen_partner_ids.filtered(lambda p: p.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -124,6 +126,7 @@ class TestDiscussFullPerformance(TransactionCase): 'message_unread_counter': 5, 'name': 'general', 'public': 'groups', + 'rtcSessions': [('insert-and-replace', [])], 'seen_message_id': False, 'state': 'open', 'uuid': channel_general.uuid, @@ -137,6 +140,8 @@ class TestDiscussFullPerformance(TransactionCase): 'description': False, 'group_based_subscription': False, 'id': channel_channel_public_1.id, + 'invitedGuests': [('insert-and-replace', [])], + 'invitedPartners': [('insert-and-replace', [])], 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': channel_channel_public_1.channel_last_seen_partner_ids.filtered(lambda p: p.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -146,6 +151,7 @@ class TestDiscussFullPerformance(TransactionCase): 'message_unread_counter': 0, 'name': 'public 1', 'public': 'public', + 'rtcSessions': [('insert-and-replace', [])], 'seen_message_id': next(res['message_id'] for res in channel_channel_public_1._channel_last_message_ids()), 'state': 'open', 'uuid': channel_channel_public_1.uuid, @@ -159,6 +165,8 @@ class TestDiscussFullPerformance(TransactionCase): 'description': False, 'group_based_subscription': False, 'id': channel_channel_public_2.id, + 'invitedGuests': [('insert-and-replace', [])], + 'invitedPartners': [('insert-and-replace', [])], 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': channel_channel_public_2.channel_last_seen_partner_ids.filtered(lambda p: p.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -168,6 +176,7 @@ class TestDiscussFullPerformance(TransactionCase): 'message_unread_counter': 0, 'name': 'public 2', 'public': 'public', + 'rtcSessions': [('insert-and-replace', [])], 'seen_message_id': next(res['message_id'] for res in channel_channel_public_2._channel_last_message_ids()), 'state': 'open', 'uuid': channel_channel_public_2.uuid, @@ -181,6 +190,8 @@ class TestDiscussFullPerformance(TransactionCase): 'description': False, 'group_based_subscription': False, 'id': channel_channel_group_1.id, + 'invitedGuests': [('insert-and-replace', [])], + 'invitedPartners': [('insert-and-replace', [])], 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': channel_channel_group_1.channel_last_seen_partner_ids.filtered(lambda p: p.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -190,6 +201,7 @@ class TestDiscussFullPerformance(TransactionCase): 'message_unread_counter': 0, 'name': 'group 1', 'public': 'groups', + 'rtcSessions': [('insert-and-replace', [])], 'seen_message_id': next(res['message_id'] for res in channel_channel_group_1._channel_last_message_ids()), 'state': 'open', 'uuid': channel_channel_group_1.uuid, @@ -203,6 +215,8 @@ class TestDiscussFullPerformance(TransactionCase): 'description': False, 'group_based_subscription': False, 'id': channel_channel_group_2.id, + 'invitedGuests': [('insert-and-replace', [])], + 'invitedPartners': [('insert-and-replace', [])], 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': channel_channel_group_2.channel_last_seen_partner_ids.filtered(lambda p: p.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -212,6 +226,7 @@ class TestDiscussFullPerformance(TransactionCase): 'message_unread_counter': 0, 'name': 'group 2', 'public': 'groups', + 'rtcSessions': [('insert-and-replace', [])], 'seen_message_id': next(res['message_id'] for res in channel_channel_group_2._channel_last_message_ids()), 'state': 'open', 'uuid': channel_channel_group_2.uuid, @@ -225,6 +240,8 @@ class TestDiscussFullPerformance(TransactionCase): 'description': False, 'group_based_subscription': False, 'id': channel_channel_private_1.id, + 'invitedGuests': [('insert-and-replace', [])], + 'invitedPartners': [('insert-and-replace', [])], 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': channel_channel_private_1.channel_last_seen_partner_ids.filtered(lambda p: p.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -234,6 +251,7 @@ class TestDiscussFullPerformance(TransactionCase): 'message_unread_counter': 0, 'name': 'private 1', 'public': 'private', + 'rtcSessions': [('insert-and-replace', [])], 'seen_message_id': next(res['message_id'] for res in channel_channel_private_1._channel_last_message_ids()), 'state': 'open', 'uuid': channel_channel_private_1.uuid, @@ -247,6 +265,8 @@ class TestDiscussFullPerformance(TransactionCase): 'description': False, 'group_based_subscription': False, 'id': channel_channel_private_2.id, + 'invitedGuests': [('insert-and-replace', [])], + 'invitedPartners': [('insert-and-replace', [])], 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': channel_channel_private_2.channel_last_seen_partner_ids.filtered(lambda p: p.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -256,6 +276,7 @@ class TestDiscussFullPerformance(TransactionCase): 'message_unread_counter': 0, 'name': 'private 2', 'public': 'private', + 'rtcSessions': [('insert-and-replace', [])], 'seen_message_id': next(res['message_id'] for res in channel_channel_private_2._channel_last_message_ids()), 'state': 'open', 'uuid': channel_channel_private_2.uuid, @@ -269,6 +290,8 @@ class TestDiscussFullPerformance(TransactionCase): 'description': False, 'group_based_subscription': False, 'id': channel_group_1.id, + 'invitedGuests': [('insert-and-replace', [])], + 'invitedPartners': [('insert-and-replace', [])], 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': channel_group_1.channel_last_seen_partner_ids.filtered(lambda p: p.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -302,6 +325,7 @@ class TestDiscussFullPerformance(TransactionCase): 'message_unread_counter': 0, 'name': '', 'public': 'private', + 'rtcSessions': [('insert-and-replace', [])], 'seen_message_id': False, 'seen_partners_info': [ { @@ -329,6 +353,8 @@ class TestDiscussFullPerformance(TransactionCase): 'description': False, 'group_based_subscription': False, 'id': channel_chat_1.id, + 'invitedGuests': [('insert-and-replace', [])], + 'invitedPartners': [('insert-and-replace', [])], 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': channel_chat_1.channel_last_seen_partner_ids.filtered(lambda p: p.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -362,6 +388,7 @@ class TestDiscussFullPerformance(TransactionCase): 'message_unread_counter': 0, 'name': 'Ernest Employee, test14', 'public': 'private', + 'rtcSessions': [('insert-and-replace', [])], 'seen_partners_info': [ { 'fetched_message_id': False, @@ -389,6 +416,8 @@ class TestDiscussFullPerformance(TransactionCase): 'description': False, 'group_based_subscription': False, 'id': channel_chat_2.id, + 'invitedGuests': [('insert-and-replace', [])], + 'invitedPartners': [('insert-and-replace', [])], 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': channel_chat_2.channel_last_seen_partner_ids.filtered(lambda p: p.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -422,6 +451,7 @@ class TestDiscussFullPerformance(TransactionCase): 'message_unread_counter': 0, 'name': 'Ernest Employee, test15', 'public': 'private', + 'rtcSessions': [('insert-and-replace', [])], 'seen_partners_info': [ { 'fetched_message_id': False, @@ -449,6 +479,8 @@ class TestDiscussFullPerformance(TransactionCase): 'description': False, 'group_based_subscription': False, 'id': channel_chat_3.id, + 'invitedGuests': [('insert-and-replace', [])], + 'invitedPartners': [('insert-and-replace', [])], 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': channel_chat_3.channel_last_seen_partner_ids.filtered(lambda p: p.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -482,6 +514,7 @@ class TestDiscussFullPerformance(TransactionCase): 'message_unread_counter': 0, 'name': 'Ernest Employee, test2', 'public': 'private', + 'rtcSessions': [('insert-and-replace', [])], 'seen_partners_info': [ { 'fetched_message_id': False, @@ -509,6 +542,8 @@ class TestDiscussFullPerformance(TransactionCase): 'description': False, 'group_based_subscription': False, 'id': channel_chat_4.id, + 'invitedGuests': [('insert-and-replace', [])], + 'invitedPartners': [('insert-and-replace', [])], 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': channel_chat_4.channel_last_seen_partner_ids.filtered(lambda p: p.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -542,6 +577,7 @@ class TestDiscussFullPerformance(TransactionCase): 'message_unread_counter': 0, 'name': 'Ernest Employee, test3', 'public': 'private', + 'rtcSessions': [('insert-and-replace', [])], 'seen_partners_info': [ { 'fetched_message_id': False, @@ -569,6 +605,8 @@ class TestDiscussFullPerformance(TransactionCase): 'description': False, 'group_based_subscription': False, 'id': channel_livechat_1.id, + 'invitedGuests': [('insert-and-replace', [])], + 'invitedPartners': [('insert-and-replace', [])], 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': channel_livechat_1.channel_last_seen_partner_ids.filtered(lambda p: p.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -608,6 +646,7 @@ class TestDiscussFullPerformance(TransactionCase): 'name': 'test1 Ernest Employee', 'operator_pid': (self.users[0].partner_id.id, 'Ernest Employee'), 'public': 'private', + 'rtcSessions': [('insert-and-replace', [])], 'seen_partners_info': [ { 'fetched_message_id': False, @@ -635,6 +674,8 @@ class TestDiscussFullPerformance(TransactionCase): 'description': False, 'group_based_subscription': False, 'id': channel_livechat_2.id, + 'invitedGuests': [('insert-and-replace', [])], + 'invitedPartners': [('insert-and-replace', [])], 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': channel_livechat_2.channel_last_seen_partner_ids.filtered(lambda p: p.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -674,6 +715,7 @@ class TestDiscussFullPerformance(TransactionCase): 'name': 'anon 2 Ernest Employee', 'operator_pid': (self.users[0].partner_id.id, 'Ernest Employee'), 'public': 'private', + 'rtcSessions': [('insert-and-replace', [])], 'seen_partners_info': [ { 'fetched_message_id': next(res['message_id'] for res in channel_livechat_2._channel_last_message_ids()),