mail_channel_member.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  2. from werkzeug.exceptions import NotFound
  3. from odoo import api, fields, models, _
  4. from odoo.exceptions import AccessError
  5. from odoo.osv import expression
  6. class ChannelMember(models.Model):
  7. _name = 'mail.channel.member'
  8. _description = 'Listeners of a Channel'
  9. _table = 'mail_channel_member'
  10. _rec_names_search = ['partner_id', 'guest_id']
  11. _bypass_create_check = {}
  12. # identity
  13. partner_id = fields.Many2one('res.partner', string='Recipient', ondelete='cascade', index=True)
  14. guest_id = fields.Many2one(string="Guest", comodel_name='mail.guest', ondelete='cascade', readonly=True, index=True)
  15. partner_email = fields.Char('Email', related='partner_id.email', related_sudo=False)
  16. # channel
  17. channel_id = fields.Many2one('mail.channel', string='Channel', ondelete='cascade', readonly=True, required=True)
  18. # state
  19. custom_channel_name = fields.Char('Custom channel name')
  20. fetched_message_id = fields.Many2one('mail.message', string='Last Fetched')
  21. seen_message_id = fields.Many2one('mail.message', string='Last Seen')
  22. message_unread_counter = fields.Integer('Unread Messages Counter', compute='_compute_message_unread', compute_sudo=True)
  23. fold_state = fields.Selection([('open', 'Open'), ('folded', 'Folded'), ('closed', 'Closed')], string='Conversation Fold State', default='open')
  24. is_minimized = fields.Boolean("Conversation is minimized")
  25. is_pinned = fields.Boolean("Is pinned on the interface", default=True)
  26. 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.")
  27. last_seen_dt = fields.Datetime("Last seen date")
  28. # RTC
  29. rtc_session_ids = fields.One2many(string="RTC Sessions", comodel_name='mail.channel.rtc.session', inverse_name='channel_member_id')
  30. rtc_inviting_session_id = fields.Many2one('mail.channel.rtc.session', string='Ringing session')
  31. @api.depends('channel_id.message_ids', 'seen_message_id')
  32. def _compute_message_unread(self):
  33. if self.ids:
  34. self.env['mail.message'].flush_model()
  35. self.flush_recordset(['channel_id', 'seen_message_id'])
  36. self.env.cr.execute("""
  37. SELECT count(mail_message.id) AS count,
  38. mail_channel_member.id
  39. FROM mail_message
  40. INNER JOIN mail_channel_member
  41. ON mail_channel_member.channel_id = mail_message.res_id
  42. WHERE mail_message.model = 'mail.channel'
  43. AND mail_message.message_type NOT IN ('notification', 'user_notification')
  44. AND (
  45. mail_message.id > mail_channel_member.seen_message_id
  46. OR mail_channel_member.seen_message_id IS NULL
  47. )
  48. AND mail_channel_member.id IN %(ids)s
  49. GROUP BY mail_channel_member.id
  50. """, {'ids': tuple(self.ids)})
  51. unread_counter_by_member = {res['id']: res['count'] for res in self.env.cr.dictfetchall()}
  52. for member in self:
  53. member.message_unread_counter = unread_counter_by_member.get(member.id)
  54. else:
  55. self.message_unread_counter = 0
  56. def name_get(self):
  57. return [(record.id, record.partner_id.name or record.guest_id.name) for record in self]
  58. def init(self):
  59. self.env.cr.execute("CREATE UNIQUE INDEX IF NOT EXISTS mail_channel_member_partner_unique ON %s (channel_id, partner_id) WHERE partner_id IS NOT NULL" % self._table)
  60. self.env.cr.execute("CREATE UNIQUE INDEX IF NOT EXISTS mail_channel_member_guest_unique ON %s (channel_id, guest_id) WHERE guest_id IS NOT NULL" % self._table)
  61. _sql_constraints = [
  62. ("partner_or_guest_exists", "CHECK((partner_id IS NOT NULL AND guest_id IS NULL) OR (partner_id IS NULL AND guest_id IS NOT NULL))", "A channel member must be a partner or a guest."),
  63. ]
  64. @api.model_create_multi
  65. def create(self, vals_list):
  66. """Similar access rule as the access rule of the mail channel.
  67. It can not be implemented in XML, because when the record will be created, the
  68. partner will be added in the channel and the security rule will always authorize
  69. the creation.
  70. """
  71. if not self.env.is_admin() and not self.env.context.get('mail_create_bypass_create_check') is self._bypass_create_check:
  72. for vals in vals_list:
  73. if 'channel_id' in vals:
  74. channel_id = self.env['mail.channel'].browse(vals['channel_id'])
  75. if not channel_id._can_invite(vals.get('partner_id')):
  76. raise AccessError(_('This user can not be added in this channel'))
  77. return super().create(vals_list)
  78. def write(self, vals):
  79. for channel_member in self:
  80. for field_name in {'channel_id', 'partner_id', 'guest_id'}:
  81. if field_name in vals and vals[field_name] != channel_member[field_name].id:
  82. raise AccessError(_('You can not write on %(field_name)s.', field_name=field_name))
  83. return super().write(vals)
  84. def unlink(self):
  85. self.sudo().rtc_session_ids.unlink()
  86. return super().unlink()
  87. @api.model
  88. def _get_as_sudo_from_request_or_raise(self, request, channel_id):
  89. channel_member = self._get_as_sudo_from_request(request=request, channel_id=channel_id)
  90. if not channel_member:
  91. raise NotFound()
  92. return channel_member
  93. @api.model
  94. def _get_as_sudo_from_request(self, request, channel_id):
  95. """ Seeks a channel member matching the provided `channel_id` and the
  96. current user or guest.
  97. :param channel_id: The id of the channel of which the user/guest is
  98. expected to be member.
  99. :type channel_id: int
  100. :return: A record set containing the channel member if found, or an
  101. empty record set otherwise. In case of guest, the record is returned
  102. with the 'guest' record in the context.
  103. :rtype: mail.channel.member
  104. """
  105. if request.session.uid:
  106. return self.env['mail.channel.member'].sudo().search([('channel_id', '=', channel_id), ('partner_id', '=', self.env.user.partner_id.id)], limit=1)
  107. guest = self.env['mail.guest']._get_guest_from_request(request)
  108. if guest:
  109. return guest.env['mail.channel.member'].sudo().search([('channel_id', '=', channel_id), ('guest_id', '=', guest.id)], limit=1)
  110. return self.env['mail.channel.member'].sudo()
  111. def _notify_typing(self, is_typing):
  112. """ Broadcast the typing notification to channel members
  113. :param is_typing: (boolean) tells whether the members are typing or not
  114. """
  115. notifications = []
  116. for member in self:
  117. formatted_member = member._mail_channel_member_format().get(member)
  118. formatted_member['isTyping'] = is_typing
  119. notifications.append([member.channel_id, 'mail.channel.member/typing_status', formatted_member])
  120. notifications.append([member.channel_id.uuid, 'mail.channel.member/typing_status', formatted_member]) # notify livechat users
  121. self.env['bus.bus']._sendmany(notifications)
  122. def _mail_channel_member_format(self, fields=None):
  123. if not fields:
  124. fields = {'id': True, 'channel': {}, 'persona': {}}
  125. members_formatted_data = {}
  126. for member in self:
  127. data = {}
  128. if 'id' in fields:
  129. data['id'] = member.id
  130. if 'channel' in fields:
  131. data['channel'] = member.channel_id._channel_format(fields=fields.get('channel')).get(member.channel_id)
  132. if 'persona' in fields:
  133. if member.partner_id:
  134. persona = {'partner': member._get_partner_data(fields=fields.get('persona', {}).get('partner'))}
  135. if member.guest_id:
  136. persona = {'guest': member.guest_id.sudo()._guest_format(fields=fields.get('persona', {}).get('guest')).get(member.guest_id)}
  137. data['persona'] = persona
  138. members_formatted_data[member] = data
  139. return members_formatted_data
  140. def _get_partner_data(self, fields=None):
  141. self.ensure_one()
  142. return self.partner_id.mail_partner_format(fields=fields).get(self.partner_id)
  143. # --------------------------------------------------------------------------
  144. # RTC (voice/video)
  145. # --------------------------------------------------------------------------
  146. def _rtc_join_call(self, check_rtc_session_ids=None):
  147. self.ensure_one()
  148. check_rtc_session_ids = (check_rtc_session_ids or []) + self.rtc_session_ids.ids
  149. self.channel_id._rtc_cancel_invitations(member_ids=self.ids)
  150. self.rtc_session_ids.unlink()
  151. rtc_session = self.env['mail.channel.rtc.session'].create({'channel_member_id': self.id})
  152. current_rtc_sessions, outdated_rtc_sessions = self._rtc_sync_sessions(check_rtc_session_ids=check_rtc_session_ids)
  153. res = {
  154. 'iceServers': self.env['mail.ice.server']._get_ice_servers() or False,
  155. 'rtcSessions': [
  156. ('insert', [rtc_session_sudo._mail_rtc_session_format() for rtc_session_sudo in current_rtc_sessions]),
  157. ('insert-and-unlink', [{'id': missing_rtc_session_sudo.id} for missing_rtc_session_sudo in outdated_rtc_sessions]),
  158. ],
  159. 'sessionId': rtc_session.id,
  160. }
  161. if len(self.channel_id.rtc_session_ids) == 1 and self.channel_id.channel_type in {'chat', 'group'}:
  162. self.channel_id.message_post(body=_("%s started a live conference", self.partner_id.name or self.guest_id.name), message_type='notification')
  163. invited_members = self._rtc_invite_members()
  164. if invited_members:
  165. res['invitedMembers'] = [('insert', list(invited_members._mail_channel_member_format(fields={'id': True, 'channel': {}, 'persona': {'partner': {'id', 'name', 'im_status'}, 'guest': {'id', 'name', 'im_status'}}}).values()))]
  166. return res
  167. def _rtc_leave_call(self):
  168. self.ensure_one()
  169. if self.rtc_session_ids:
  170. self.rtc_session_ids.unlink()
  171. else:
  172. return self.channel_id._rtc_cancel_invitations(member_ids=self.ids)
  173. def _rtc_sync_sessions(self, check_rtc_session_ids=None):
  174. """Synchronize the RTC sessions for self channel member.
  175. - Inactive sessions of the channel are deleted.
  176. - Current sessions are returned.
  177. - Sessions given in check_rtc_session_ids that no longer exists
  178. are returned as non-existing.
  179. :param list check_rtc_session_ids: list of the ids of the sessions to check
  180. :returns tuple: (current_rtc_sessions, outdated_rtc_sessions)
  181. """
  182. self.ensure_one()
  183. self.channel_id.rtc_session_ids._delete_inactive_rtc_sessions()
  184. check_rtc_sessions = self.env['mail.channel.rtc.session'].browse([int(check_rtc_session_id) for check_rtc_session_id in (check_rtc_session_ids or [])])
  185. return self.channel_id.rtc_session_ids, check_rtc_sessions - self.channel_id.rtc_session_ids
  186. def _rtc_invite_members(self, member_ids=None):
  187. """ Sends invitations to join the RTC call to all connected members of the thread who are not already invited,
  188. if member_ids is set, only the specified ids will be invited.
  189. :param list member_ids: list of the partner ids to invite
  190. """
  191. self.ensure_one()
  192. channel_member_domain = [
  193. ('channel_id', '=', self.channel_id.id),
  194. ('rtc_inviting_session_id', '=', False),
  195. ('rtc_session_ids', '=', False),
  196. ]
  197. if member_ids:
  198. channel_member_domain = expression.AND([channel_member_domain, [('id', 'in', member_ids)]])
  199. invitation_notifications = []
  200. members = self.env['mail.channel.member'].search(channel_member_domain)
  201. for member in members:
  202. member.rtc_inviting_session_id = self.rtc_session_ids.id
  203. if member.partner_id:
  204. target = member.partner_id
  205. else:
  206. target = member.guest_id
  207. invitation_notifications.append((target, 'mail.thread/insert', {
  208. 'id': self.channel_id.id,
  209. 'model': 'mail.channel',
  210. 'rtcInvitingSession': self.rtc_session_ids._mail_rtc_session_format(),
  211. }))
  212. self.env['bus.bus']._sendmany(invitation_notifications)
  213. if members:
  214. channel_data = {'id': self.channel_id.id, 'model': 'mail.channel'}
  215. channel_data['invitedMembers'] = [('insert', list(members._mail_channel_member_format(fields={'id': True, 'channel': {}, 'persona': {'partner': {'id', 'name', 'im_status'}, 'guest': {'id', 'name', 'im_status'}}}).values()))]
  216. self.env['bus.bus']._sendone(self.channel_id, 'mail.thread/insert', channel_data)
  217. return members