160 lines
7.4 KiB
Python
160 lines
7.4 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import logging
|
|
import requests
|
|
|
|
from collections import defaultdict
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from odoo import api, fields, models
|
|
from odoo.addons.mail.tools import discuss, jwt
|
|
from odoo.addons.mail.tools.discuss import Store
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class MailRtcSession(models.Model):
|
|
_name = 'discuss.channel.rtc.session'
|
|
_inherit = ["bus.listener.mixin"]
|
|
_description = 'Mail RTC session'
|
|
_rec_name = 'channel_member_id'
|
|
|
|
channel_member_id = fields.Many2one('discuss.channel.member', required=True, ondelete='cascade')
|
|
channel_id = fields.Many2one('discuss.channel', related='channel_member_id.channel_id', store=True, readonly=True)
|
|
partner_id = fields.Many2one('res.partner', related='channel_member_id.partner_id', string="Partner")
|
|
guest_id = fields.Many2one('mail.guest', related='channel_member_id.guest_id')
|
|
|
|
write_date = fields.Datetime("Last Updated On", index=True)
|
|
|
|
is_screen_sharing_on = fields.Boolean(string="Is sharing the screen")
|
|
is_camera_on = fields.Boolean(string="Is sending user video")
|
|
is_muted = fields.Boolean(string="Is microphone muted")
|
|
is_deaf = fields.Boolean(string="Has disabled incoming sound")
|
|
|
|
_sql_constraints = [
|
|
('channel_member_unique', 'UNIQUE(channel_member_id)',
|
|
'There can only be one rtc session per channel member')
|
|
]
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
rtc_sessions = super().create(vals_list)
|
|
rtc_sessions_by_channel = defaultdict(lambda: self.env["discuss.channel.rtc.session"])
|
|
for rtc_session in rtc_sessions:
|
|
rtc_sessions_by_channel[rtc_session.channel_id] += rtc_session
|
|
for channel, rtc_sessions in rtc_sessions_by_channel.items():
|
|
channel._bus_send_store(channel, {"rtcSessions": Store.many(rtc_sessions, "ADD")})
|
|
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()
|
|
# If there is no member left in the RTC call, we remove the SFU channel uuid as the SFU
|
|
# server will timeout the channel. It is better to obtain a new channel from the SFU server
|
|
# than to attempt recycling a possibly stale channel uuid.
|
|
channel.sfu_channel_uuid = False
|
|
channel.sfu_server_url = False
|
|
rtc_sessions_by_channel = defaultdict(lambda: self.env["discuss.channel.rtc.session"])
|
|
for rtc_session in self:
|
|
rtc_sessions_by_channel[rtc_session.channel_id] += rtc_session
|
|
for channel, rtc_sessions in rtc_sessions_by_channel.items():
|
|
channel._bus_send_store(
|
|
channel, {"rtcSessions": Store.many(rtc_sessions, "DELETE", only_id=True)}
|
|
)
|
|
for rtc_session in self:
|
|
rtc_session._bus_send(
|
|
"discuss.channel.rtc.session/ended", {"sessionId": rtc_session.id}
|
|
)
|
|
return super().unlink()
|
|
|
|
def _bus_channel(self):
|
|
return self.channel_member_id._bus_channel()
|
|
|
|
def _update_and_broadcast(self, values):
|
|
""" Updates the session and notifies all members of the channel
|
|
of the change.
|
|
"""
|
|
valid_values = {'is_screen_sharing_on', 'is_camera_on', 'is_muted', 'is_deaf'}
|
|
self.write({key: values[key] for key in valid_values if key in values})
|
|
store = Store(self, extra=True)
|
|
self.channel_id._bus_send(
|
|
"discuss.channel.rtc.session/update_and_broadcast",
|
|
{"data": store.get_result(), "channelId": self.channel_id.id},
|
|
)
|
|
|
|
@api.autovacuum
|
|
def _gc_inactive_sessions(self):
|
|
""" Garbage collect sessions that aren't active anymore,
|
|
this can happen when the server or the user's browser crash
|
|
or when the user's odoo session ends.
|
|
"""
|
|
self.search(self._inactive_rtc_session_domain()).unlink()
|
|
|
|
def action_disconnect(self):
|
|
session_ids_by_channel_by_url = defaultdict(lambda: defaultdict(list))
|
|
for rtc_session in self:
|
|
sfu_channel_uuid = rtc_session.channel_id.sfu_channel_uuid
|
|
url = rtc_session.channel_id.sfu_server_url
|
|
if sfu_channel_uuid and url:
|
|
session_ids_by_channel_by_url[url][sfu_channel_uuid].append(rtc_session.id)
|
|
key = discuss.get_sfu_key(self.env)
|
|
if key:
|
|
with requests.Session() as requests_session:
|
|
for url, session_ids_by_channel in session_ids_by_channel_by_url.items():
|
|
try:
|
|
requests_session.post(
|
|
url + '/v1/disconnect',
|
|
data=jwt.sign({'sessionIdsByChannel': session_ids_by_channel}, key=key, ttl=20, algorithm=jwt.Algorithm.HS256),
|
|
timeout=3
|
|
).raise_for_status()
|
|
except requests.exceptions.RequestException as error:
|
|
_logger.warning("Could not disconnect sessions at sfu server %s: %s", url, error)
|
|
self.unlink()
|
|
|
|
def _delete_inactive_rtc_sessions(self):
|
|
"""Deletes the inactive sessions from self."""
|
|
self.filtered_domain(self._inactive_rtc_session_domain()).unlink()
|
|
|
|
def _notify_peers(self, notifications):
|
|
""" Used for peer-to-peer communication,
|
|
guarantees that the sender is the current guest or partner.
|
|
|
|
:param notifications: list of tuple with the following elements:
|
|
- target_session_ids: a list of discuss.channel.rtc.session ids
|
|
- content: a string with the content to be sent to the targets
|
|
"""
|
|
self.ensure_one()
|
|
payload_by_target = defaultdict(lambda: {'sender': self.id, 'notifications': []})
|
|
for target_session_ids, content in notifications:
|
|
for target_session in self.env['discuss.channel.rtc.session'].browse(target_session_ids).exists():
|
|
payload_by_target[target_session]['notifications'].append(content)
|
|
for target, payload in payload_by_target.items():
|
|
target._bus_send("discuss.channel.rtc.session/peer_notification", payload)
|
|
|
|
def _to_store(self, store: Store, extra=False):
|
|
for rtc_session in self:
|
|
data = rtc_session._read_format([], load=False)[0]
|
|
data["channelMember"] = Store.one(
|
|
rtc_session.channel_member_id,
|
|
fields={"channel": [], "persona": ["name", "im_status"]},
|
|
)
|
|
if extra:
|
|
data.update(
|
|
{
|
|
"isCameraOn": rtc_session.is_camera_on,
|
|
"isDeaf": rtc_session.is_deaf,
|
|
"isSelfMuted": rtc_session.is_muted,
|
|
"isScreenSharingOn": rtc_session.is_screen_sharing_on,
|
|
}
|
|
)
|
|
store.add(rtc_session, data)
|
|
|
|
@api.model
|
|
def _inactive_rtc_session_domain(self):
|
|
return [('write_date', '<', fields.Datetime.now() - relativedelta(minutes=1))]
|