summaryrefslogtreecommitdiff
path: root/addons/mail/models/mail_channel.py
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/mail/models/mail_channel.py
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/mail/models/mail_channel.py')
-rw-r--r--addons/mail/models/mail_channel.py1149
1 files changed, 1149 insertions, 0 deletions
diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py
new file mode 100644
index 00000000..e0a5ffa5
--- /dev/null
+++ b/addons/mail/models/mail_channel.py
@@ -0,0 +1,1149 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import base64
+import logging
+import re
+from uuid import uuid4
+
+from odoo import _, api, fields, models, modules, tools
+from odoo.exceptions import UserError, ValidationError
+from odoo.osv import expression
+from odoo.tools import ormcache, formataddr
+from odoo.exceptions import AccessError
+from odoo.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG
+
+MODERATION_FIELDS = ['moderation', 'moderator_ids', 'moderation_ids', 'moderation_notify', 'moderation_notify_msg', 'moderation_guidelines', 'moderation_guidelines_msg']
+_logger = logging.getLogger(__name__)
+
+
+class ChannelPartner(models.Model):
+ _name = 'mail.channel.partner'
+ _description = 'Listeners of a Channel'
+ _table = 'mail_channel_partner'
+ _rec_name = 'partner_id'
+
+ custom_channel_name = fields.Char('Custom channel name')
+ partner_id = fields.Many2one('res.partner', string='Recipient', ondelete='cascade')
+ partner_email = fields.Char('Email', related='partner_id.email', depends=['partner_id'], readonly=False)
+ channel_id = fields.Many2one('mail.channel', string='Channel', ondelete='cascade')
+ fetched_message_id = fields.Many2one('mail.message', string='Last Fetched')
+ seen_message_id = fields.Many2one('mail.message', string='Last Seen')
+ fold_state = fields.Selection([('open', 'Open'), ('folded', 'Folded'), ('closed', 'Closed')], string='Conversation Fold State', default='open')
+ is_minimized = fields.Boolean("Conversation is minimized")
+ is_pinned = fields.Boolean("Is pinned on the interface", default=True)
+
+ @api.model
+ def create(self, vals):
+ """Similar access rule as the access rule of the mail channel.
+
+ It can not be implemented in XML, because when the record will be created, the
+ partner will be added in the channel and the security rule will always authorize
+ the creation.
+ """
+ if 'channel_id' in vals and not self.env.is_admin():
+ channel_id = self.env['mail.channel'].browse(vals['channel_id'])
+ if not channel_id._can_invite(vals.get('partner_id')):
+ raise AccessError(_('This user can not be added in this channel'))
+ return super(ChannelPartner, self).create(vals)
+
+ def write(self, vals):
+ if not self.env.is_admin():
+ if {'channel_id', 'partner_id', 'partner_email'} & set(vals):
+ raise AccessError(_('You can not write on this field'))
+ return super(ChannelPartner, self).write(vals)
+
+
+class Moderation(models.Model):
+ _name = 'mail.moderation'
+ _description = 'Channel black/white list'
+
+ email = fields.Char(string="Email", index=True, required=True)
+ status = fields.Selection([
+ ('allow', 'Always Allow'),
+ ('ban', 'Permanent Ban')],
+ string="Status", required=True)
+ channel_id = fields.Many2one('mail.channel', string="Channel", index=True, required=True)
+
+ _sql_constraints = [
+ ('channel_email_uniq', 'unique (email,channel_id)', 'The email address must be unique per channel !')
+ ]
+
+
+class Channel(models.Model):
+ """ A mail.channel is a discussion group that may behave like a listener
+ on documents. """
+ _description = 'Discussion Channel'
+ _name = 'mail.channel'
+ _mail_flat_thread = False
+ _mail_post_access = 'read'
+ _inherit = ['mail.thread', 'mail.alias.mixin']
+
+ MAX_BOUNCE_LIMIT = 10
+
+ @api.model
+ def default_get(self, fields):
+ res = super(Channel, self).default_get(fields)
+ if not res.get('alias_contact') and (not fields or 'alias_contact' in fields):
+ res['alias_contact'] = 'everyone' if res.get('public', 'private') == 'public' else 'followers'
+ return res
+
+ def _get_default_image(self):
+ image_path = modules.get_module_resource('mail', 'static/src/img', 'groupdefault.png')
+ return base64.b64encode(open(image_path, 'rb').read())
+
+ name = fields.Char('Name', required=True, translate=True)
+ active = fields.Boolean(default=True, help="Set active to false to hide the channel without removing it.")
+ channel_type = fields.Selection([
+ ('chat', 'Chat Discussion'),
+ ('channel', 'Channel')],
+ 'Channel Type', default='channel')
+ is_chat = fields.Boolean(string='Is a chat', compute='_compute_is_chat', default=False)
+ description = fields.Text('Description')
+ uuid = fields.Char('UUID', size=50, index=True, default=lambda self: str(uuid4()), copy=False)
+ email_send = fields.Boolean('Send messages by email', default=False)
+ # multi users channel
+ # depends=['...'] is for `test_mail/tests/common.py`, class Moderation, `setUpClass`
+ channel_last_seen_partner_ids = fields.One2many('mail.channel.partner', 'channel_id', string='Last Seen', depends=['channel_partner_ids'])
+ channel_partner_ids = fields.Many2many('res.partner', 'mail_channel_partner', 'channel_id', 'partner_id', string='Listeners', depends=['channel_last_seen_partner_ids'])
+ channel_message_ids = fields.Many2many('mail.message', 'mail_message_mail_channel_rel')
+ is_member = fields.Boolean('Is a member', compute='_compute_is_member')
+ # access
+ public = fields.Selection([
+ ('public', 'Everyone'),
+ ('private', 'Invited people only'),
+ ('groups', 'Selected group of users')],
+ 'Privacy', required=True, default='groups',
+ help='This group is visible by non members. Invisible groups can add members through the invite button.')
+ group_public_id = fields.Many2one('res.groups', string='Authorized Group',
+ default=lambda self: self.env.ref('base.group_user'))
+ group_ids = fields.Many2many(
+ 'res.groups', string='Auto Subscription',
+ help="Members of those groups will automatically added as followers. "
+ "Note that they will be able to manage their subscription manually "
+ "if necessary.")
+ image_128 = fields.Image("Image", max_width=128, max_height=128, default=_get_default_image)
+ is_subscribed = fields.Boolean(
+ 'Is Subscribed', compute='_compute_is_subscribed')
+ # moderation
+ moderation = fields.Boolean(string='Moderate this channel')
+ moderator_ids = fields.Many2many('res.users', 'mail_channel_moderator_rel', string='Moderators')
+ is_moderator = fields.Boolean(help="Current user is a moderator of the channel", string='Moderator', compute="_compute_is_moderator")
+ moderation_ids = fields.One2many(
+ 'mail.moderation', 'channel_id', string='Moderated Emails',
+ groups="base.group_user")
+ moderation_count = fields.Integer(
+ string='Moderated emails count', compute='_compute_moderation_count',
+ groups="base.group_user")
+ moderation_notify = fields.Boolean(string="Automatic notification", help="People receive an automatic notification about their message being waiting for moderation.")
+ moderation_notify_msg = fields.Text(string="Notification message")
+ moderation_guidelines = fields.Boolean(string="Send guidelines to new subscribers", help="Newcomers on this moderated channel will automatically receive the guidelines.")
+ moderation_guidelines_msg = fields.Text(string="Guidelines")
+
+ @api.depends('channel_partner_ids')
+ def _compute_is_subscribed(self):
+ for channel in self:
+ channel.is_subscribed = self.env.user.partner_id in channel.channel_partner_ids
+
+ @api.depends('moderator_ids')
+ def _compute_is_moderator(self):
+ for channel in self:
+ channel.is_moderator = self.env.user in channel.moderator_ids
+
+ @api.depends('moderation_ids')
+ def _compute_moderation_count(self):
+ read_group_res = self.env['mail.moderation'].read_group([('channel_id', 'in', self.ids)], ['channel_id'], 'channel_id')
+ data = dict((res['channel_id'][0], res['channel_id_count']) for res in read_group_res)
+ for channel in self:
+ channel.moderation_count = data.get(channel.id, 0)
+
+ @api.constrains('moderator_ids')
+ def _check_moderator_email(self):
+ if any(not moderator.email for channel in self for moderator in channel.moderator_ids):
+ raise ValidationError(_("Moderators must have an email address."))
+
+ @api.constrains('moderator_ids', 'channel_partner_ids', 'channel_last_seen_partner_ids')
+ def _check_moderator_is_member(self):
+ for channel in self:
+ if not (channel.mapped('moderator_ids.partner_id') <= channel.sudo().channel_partner_ids):
+ raise ValidationError(_("Moderators should be members of the channel they moderate."))
+
+ @api.constrains('moderation', 'email_send')
+ def _check_moderation_parameters(self):
+ if any(not channel.email_send and channel.moderation for channel in self):
+ raise ValidationError(_('Only mailing lists can be moderated.'))
+
+ @api.constrains('moderator_ids')
+ def _check_moderator_existence(self):
+ if any(not channel.moderator_ids for channel in self if channel.moderation):
+ raise ValidationError(_('Moderated channels must have moderators.'))
+
+ def _compute_is_member(self):
+ memberships = self.env['mail.channel.partner'].sudo().search([
+ ('channel_id', 'in', self.ids),
+ ('partner_id', '=', self.env.user.partner_id.id),
+ ])
+ membership_ids = memberships.mapped('channel_id')
+ for record in self:
+ record.is_member = record in membership_ids
+
+ def _compute_is_chat(self):
+ for record in self:
+ if record.channel_type == 'chat':
+ record.is_chat = True
+ else:
+ record.is_chat = False
+
+ @api.onchange('public')
+ def _onchange_public(self):
+ if self.public != 'public' and self.alias_contact == 'everyone':
+ self.alias_contact = 'followers'
+
+ @api.onchange('moderator_ids')
+ def _onchange_moderator_ids(self):
+ missing_partner_ids = set(self.mapped('moderator_ids.partner_id').ids) - set(self.mapped('channel_last_seen_partner_ids.partner_id').ids)
+ if missing_partner_ids:
+ self.channel_last_seen_partner_ids = [
+ (0, 0, {'partner_id': partner_id})
+ for partner_id in missing_partner_ids
+ ]
+
+ @api.onchange('email_send')
+ def _onchange_email_send(self):
+ if not self.email_send:
+ self.moderation = False
+
+ @api.onchange('moderation')
+ def _onchange_moderation(self):
+ if not self.moderation:
+ self.moderation_notify = False
+ self.moderation_guidelines = False
+ self.moderator_ids = False
+ else:
+ self.moderator_ids |= self.env.user
+
+ @api.model
+ def create(self, vals):
+ # ensure image at quick create
+ if not vals.get('image_128'):
+ defaults = self.default_get(['image_128'])
+ vals['image_128'] = defaults['image_128']
+
+ current_partner = self.env.user.partner_id.id
+ # always add current user to new channel, go through
+ # channel_last_seen_partner_ids otherwise in v14 the channel is not
+ # visible for the user (because is_pinned is false and taken in account)
+ if 'channel_partner_ids' in vals:
+ vals['channel_partner_ids'] = [
+ entry
+ for entry in vals['channel_partner_ids']
+ if entry[0] != 4 or entry[1] != current_partner
+ ]
+ membership = vals.setdefault('channel_last_seen_partner_ids', [])
+ if all(entry[0] != 0 or entry[2].get('partner_id') != current_partner for entry in membership):
+ membership.append((0, False, {'partner_id': current_partner}))
+
+ visibility_default = self._fields['public'].default(self)
+ visibility = vals.pop('public', visibility_default)
+ vals['public'] = 'public'
+ # Create channel and alias
+ channel = super(Channel, self.with_context(
+ mail_create_nolog=True, mail_create_nosubscribe=True)
+ ).create(vals)
+ if visibility != 'public':
+ channel.sudo().public = visibility
+
+ if vals.get('group_ids'):
+ channel._subscribe_users()
+
+ # make channel listen itself: posting on a channel notifies the channel
+ if not self._context.get('mail_channel_noautofollow'):
+ channel.message_subscribe(channel_ids=[channel.id])
+
+ return channel
+
+ def unlink(self):
+ # Delete mail.channel
+ try:
+ all_emp_group = self.env.ref('mail.channel_all_employees')
+ except ValueError:
+ all_emp_group = None
+ if all_emp_group and all_emp_group in self and not self._context.get(MODULE_UNINSTALL_FLAG):
+ raise UserError(_('You cannot delete those groups, as the Whole Company group is required by other modules.'))
+ return super(Channel, self).unlink()
+
+ def write(self, vals):
+ # First checks if user tries to modify moderation fields and has not the right to do it.
+ if any(key for key in MODERATION_FIELDS if vals.get(key)) and any(self.env.user not in channel.moderator_ids for channel in self if channel.moderation):
+ if not self.env.user.has_group('base.group_system'):
+ raise UserError(_("You do not have the rights to modify fields related to moderation on one of the channels you are modifying."))
+
+ result = super(Channel, self).write(vals)
+
+ if vals.get('group_ids'):
+ self._subscribe_users()
+
+ # avoid keeping messages to moderate and accept them
+ if vals.get('moderation') is False:
+ self.env['mail.message'].search([
+ ('moderation_status', '=', 'pending_moderation'),
+ ('model', '=', 'mail.channel'),
+ ('res_id', 'in', self.ids)
+ ])._moderate_accept()
+
+ return result
+
+ def _alias_get_creation_values(self):
+ values = super(Channel, self)._alias_get_creation_values()
+ values['alias_model_id'] = self.env['ir.model']._get('mail.channel').id
+ if self.id:
+ values['alias_force_thread_id'] = self.id
+ return values
+
+ def _subscribe_users(self):
+ to_create = []
+ for mail_channel in self:
+ partners_to_add = mail_channel.group_ids.users.partner_id - mail_channel.channel_partner_ids
+ to_create += [{
+ 'channel_id': mail_channel.id,
+ 'partner_id': partner.id,
+ } for partner in partners_to_add]
+
+ self.env['mail.channel.partner'].create(to_create)
+
+ def action_follow(self):
+ self.ensure_one()
+ channel_partner = self.mapped('channel_last_seen_partner_ids').filtered(lambda cp: cp.partner_id == self.env.user.partner_id)
+ if not channel_partner:
+ return self.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': self.env.user.partner_id.id})]})
+ return False
+
+ def action_unfollow(self):
+ return self._action_unfollow(self.env.user.partner_id)
+
+ def _action_unfollow(self, partner):
+ self.message_unsubscribe(partner.ids)
+ if partner not in self.with_context(active_test=False).channel_partner_ids:
+ return True
+ channel_info = self.channel_info('unsubscribe')[0] # must be computed before leaving the channel (access rights)
+ result = self.write({'channel_partner_ids': [(3, partner.id)]})
+ # side effect of unsubscribe that wasn't taken into account because
+ # channel_info is called before actually unpinning the channel
+ channel_info['is_pinned'] = False
+ self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', partner.id), channel_info)
+ if not self.email_send:
+ notification = _('<div class="o_mail_notification">left <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>', self.id, self.name)
+ # post 'channel left' message as root since the partner just unsubscribed from the channel
+ self.sudo().message_post(body=notification, subtype_xmlid="mail.mt_comment", author_id=partner.id)
+ return result
+
+ def _notify_get_groups(self, msg_vals=None):
+ """ All recipients of a message on a channel are considered as partners.
+ This means they will receive a minimal email, without a link to access
+ in the backend. Mailing lists should indeed send minimal emails to avoid
+ the noise. """
+ groups = super(Channel, self)._notify_get_groups(msg_vals=msg_vals)
+ for (index, (group_name, group_func, group_data)) in enumerate(groups):
+ if group_name != 'customer':
+ groups[index] = (group_name, lambda partner: False, group_data)
+ return groups
+
+ def _notify_email_header_dict(self):
+ headers = super(Channel, self)._notify_email_header_dict()
+ headers['Precedence'] = 'list'
+ # avoid out-of-office replies from MS Exchange
+ # http://blogs.technet.com/b/exchange/archive/2006/10/06/3395024.aspx
+ headers['X-Auto-Response-Suppress'] = 'OOF'
+ if self.alias_domain and self.alias_name:
+ headers['List-Id'] = '<%s.%s>' % (self.alias_name, self.alias_domain)
+ headers['List-Post'] = '<mailto:%s@%s>' % (self.alias_name, self.alias_domain)
+ # Avoid users thinking it was a personal message
+ # X-Forge-To: will replace To: after SMTP envelope is determined by ir.mail.server
+ list_to = '"%s" <%s@%s>' % (self.name, self.alias_name, self.alias_domain)
+ headers['X-Forge-To'] = list_to
+ return headers
+
+ def _message_receive_bounce(self, email, partner):
+ """ Override bounce management to unsubscribe bouncing addresses """
+ for p in partner:
+ if p.message_bounce >= self.MAX_BOUNCE_LIMIT:
+ self._action_unfollow(p)
+ return super(Channel, self)._message_receive_bounce(email, partner)
+
+ def _notify_email_recipient_values(self, recipient_ids):
+ # Excluded Blacklisted
+ whitelist = self.env['res.partner'].sudo().browse(recipient_ids).filtered(lambda p: not p.is_blacklisted)
+ # real mailing list: multiple recipients (hidden by X-Forge-To)
+ if self.alias_domain and self.alias_name:
+ return {
+ 'email_to': ','.join(formataddr((partner.name, partner.email_normalized)) for partner in whitelist if partner.email_normalized),
+ 'recipient_ids': [],
+ }
+ return super(Channel, self)._notify_email_recipient_values(whitelist.ids)
+
+ def _extract_moderation_values(self, message_type, **kwargs):
+ """ This method is used to compute moderation status before the creation
+ of a message. For this operation the message's author email address is required.
+ This address is returned with status for other computations. """
+ moderation_status = 'accepted'
+ email = ''
+ if self.moderation and message_type in ['email', 'comment']:
+ author_id = kwargs.get('author_id')
+ if author_id and isinstance(author_id, int):
+ email = self.env['res.partner'].browse([author_id]).email
+ elif author_id:
+ email = author_id.email
+ elif kwargs.get('email_from'):
+ email = tools.email_split(kwargs['email_from'])[0]
+ else:
+ email = self.env.user.email
+ if email in self.mapped('moderator_ids.email'):
+ return moderation_status, email
+ status = self.env['mail.moderation'].sudo().search([('email', '=', email), ('channel_id', 'in', self.ids)]).mapped('status')
+ if status and status[0] == 'allow':
+ moderation_status = 'accepted'
+ elif status and status[0] == 'ban':
+ moderation_status = 'rejected'
+ else:
+ moderation_status = 'pending_moderation'
+ return moderation_status, email
+
+ @api.returns('mail.message', lambda value: value.id)
+ def message_post(self, *, message_type='notification', **kwargs):
+ moderation_status, email = self._extract_moderation_values(message_type, **kwargs)
+ if moderation_status == 'rejected':
+ return self.env['mail.message']
+
+ self.filtered(lambda channel: channel.is_chat).mapped('channel_last_seen_partner_ids').sudo().write({'is_pinned': True})
+
+ # mail_post_autofollow=False is necessary to prevent adding followers
+ # when using mentions in channels. Followers should not be added to
+ # channels, and especially not automatically (because channel membership
+ # should be managed with channel.partner instead).
+ # The current client code might be setting the key to True on sending
+ # message but it is only useful when targeting customers in chatter.
+ # This value should simply be set to False in channels no matter what.
+ message = super(Channel, self.with_context(mail_create_nosubscribe=True, mail_post_autofollow=False)).message_post(message_type=message_type, moderation_status=moderation_status, **kwargs)
+
+ # Notifies the message author when his message is pending moderation if required on channel.
+ # The fields "email_from" and "reply_to" are filled in automatically by method create in model mail.message.
+ if self.moderation_notify and self.moderation_notify_msg and message_type in ['email','comment'] and moderation_status == 'pending_moderation':
+ self.env['mail.mail'].sudo().create({
+ 'author_id': self.env.user.partner_id.id,
+ 'email_from': self.env.user.company_id.catchall_formatted or self.env.user.company_id.email_formatted,
+ 'body_html': self.moderation_notify_msg,
+ 'subject': 'Re: %s' % (kwargs.get('subject', '')),
+ 'email_to': email,
+ 'auto_delete': True,
+ 'state': 'outgoing'
+ })
+ return message
+
+ def _message_post_after_hook(self, message, msg_vals):
+ """
+ Automatically set the message posted by the current user as seen for himself.
+ """
+ self._set_last_seen_message(message)
+ return super()._message_post_after_hook(message=message, msg_vals=msg_vals)
+
+ def _alias_get_error_message(self, message, message_dict, alias):
+ if alias.alias_contact == 'followers' and self.ids:
+ author = self.env['res.partner'].browse(message_dict.get('author_id', False))
+ if not author or author not in self.channel_partner_ids:
+ return _('restricted to channel members')
+ return False
+ return super(Channel, self)._alias_get_error_message(message, message_dict, alias)
+
+ def init(self):
+ self._cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('mail_channel_partner_seen_message_id_idx',))
+ if not self._cr.fetchone():
+ self._cr.execute('CREATE INDEX mail_channel_partner_seen_message_id_idx ON mail_channel_partner (channel_id,partner_id,seen_message_id)')
+
+ # --------------------------------------------------
+ # Moderation
+ # --------------------------------------------------
+
+ def send_guidelines(self):
+ """ Send guidelines to all channel members. """
+ if self.env.user in self.moderator_ids or self.env.user.has_group('base.group_system'):
+ success = self._send_guidelines(self.channel_partner_ids)
+ if not success:
+ raise UserError(_('View "mail.mail_channel_send_guidelines" was not found. No email has been sent. Please contact an administrator to fix this issue.'))
+ else:
+ raise UserError(_("Only an administrator or a moderator can send guidelines to channel members!"))
+
+ def _send_guidelines(self, partners):
+ """ Send guidelines of a given channel. Returns False if template used for guidelines
+ not found. Caller may have to handle this return value. """
+ self.ensure_one()
+ view = self.env.ref('mail.mail_channel_send_guidelines', raise_if_not_found=False)
+ if not view:
+ _logger.warning('View "mail.mail_channel_send_guidelines" was not found.')
+ return False
+ banned_emails = self.env['mail.moderation'].sudo().search([
+ ('status', '=', 'ban'),
+ ('channel_id', 'in', self.ids)
+ ]).mapped('email')
+ for partner in partners.filtered(lambda p: p.email and not (p.email in banned_emails)):
+ company = partner.company_id or self.env.company
+ create_values = {
+ 'email_from': company.catchall_formatted or company.email_formatted,
+ 'author_id': self.env.user.partner_id.id,
+ 'body_html': view._render({'channel': self, 'partner': partner}, engine='ir.qweb', minimal_qcontext=True),
+ 'subject': _("Guidelines of channel %s", self.name),
+ 'recipient_ids': [(4, partner.id)]
+ }
+ mail = self.env['mail.mail'].sudo().create(create_values)
+ return True
+
+ def _update_moderation_email(self, emails, status):
+ """ This method adds emails into either white or black of the channel list of emails
+ according to status. If an email in emails is already moderated, the method updates the email status.
+ :param emails: list of email addresses to put in white or black list of channel.
+ :param status: value is 'allow' or 'ban'. Emails are put in white list if 'allow', in black list if 'ban'.
+ """
+ self.ensure_one()
+ splitted_emails = [tools.email_split(email)[0] for email in emails if tools.email_split(email)]
+ moderated = self.env['mail.moderation'].sudo().search([
+ ('email', 'in', splitted_emails),
+ ('channel_id', 'in', self.ids)
+ ])
+ cmds = [(1, record.id, {'status': status}) for record in moderated]
+ not_moderated = [email for email in splitted_emails if email not in moderated.mapped('email')]
+ cmds += [(0, 0, {'email': email, 'status': status}) for email in not_moderated]
+ return self.write({'moderation_ids': cmds})
+
+ #------------------------------------------------------
+ # Instant Messaging API
+ #------------------------------------------------------
+ # A channel header should be broadcasted:
+ # - when adding user to channel (only to the new added partners)
+ # - when folding/minimizing a channel (only to the user making the action)
+ # A message should be broadcasted:
+ # - when a message is posted on a channel (to the channel, using _notify() method)
+
+ # Anonymous method
+ def _broadcast(self, partner_ids):
+ """ Broadcast the current channel header to the given partner ids
+ :param partner_ids : the partner to notify
+ """
+ notifications = self._channel_channel_notifications(partner_ids)
+ self.env['bus.bus'].sendmany(notifications)
+
+ def _channel_channel_notifications(self, partner_ids):
+ """ Generate the bus notifications of current channel for the given partner ids
+ :param partner_ids : the partner to send the current channel header
+ :returns list of bus notifications (tuple (bus_channe, message_content))
+ """
+ notifications = []
+ for partner in self.env['res.partner'].browse(partner_ids):
+ user_id = partner.user_ids and partner.user_ids[0] or False
+ if user_id:
+ user_channels = self.with_user(user_id).with_context(
+ allowed_company_ids=user_id.company_ids.ids
+ )
+ for channel_info in user_channels.channel_info():
+ notifications.append([(self._cr.dbname, 'res.partner', partner.id), channel_info])
+ return notifications
+
+ def _notify_thread(self, message, msg_vals=False, **kwargs):
+ # When posting a message on a mail channel, manage moderation and postpone notify users
+ if not msg_vals or msg_vals.get('moderation_status') != 'pending_moderation':
+ super(Channel, self)._notify_thread(message, msg_vals=msg_vals, **kwargs)
+ else:
+ message._notify_pending_by_chat()
+
+ def _channel_message_notifications(self, message, message_format=False):
+ """ Generate the bus notifications for the given message
+ :param message : the mail.message to sent
+ :returns list of bus notifications (tuple (bus_channe, message_content))
+ """
+ message_format = message_format or message.message_format()[0]
+ notifications = []
+ for channel in self:
+ notifications.append([(self._cr.dbname, 'mail.channel', channel.id), dict(message_format)])
+ # add uuid to allow anonymous to listen
+ if channel.public == 'public':
+ notifications.append([channel.uuid, dict(message_format)])
+ return notifications
+
+ @api.model
+ def partner_info(self, all_partners, direct_partners):
+ """
+ Return the information needed by channel to display channel members
+ :param all_partners: list of res.parner():
+ :param direct_partners: list of res.parner():
+ :returns: a list of {'id', 'name', 'email'} for each partner and adds {im_status} for direct_partners.
+ :rtype : list(dict)
+ """
+ partner_infos = {partner['id']: partner for partner in all_partners.sudo().read(['id', 'name', 'email'])}
+ # add im _status for direct_partners
+ direct_partners_im_status = {partner['id']: partner for partner in direct_partners.sudo().read(['im_status'])}
+
+ for i in direct_partners_im_status.keys():
+ partner_infos[i].update(direct_partners_im_status[i])
+
+ return partner_infos
+
+ def channel_info(self, extra_info=False):
+ """ Get the informations header for the current channels
+ :returns a list of channels values
+ :rtype : list(dict)
+ """
+ if not self:
+ return []
+ channel_infos = []
+ # all relations partner_channel on those channels
+ all_partner_channel = self.env['mail.channel.partner'].search([('channel_id', 'in', self.ids)])
+
+ # all partner infos on those channels
+ channel_dict = {channel.id: channel for channel in self}
+ all_partners = all_partner_channel.mapped('partner_id')
+ direct_channel_partners = all_partner_channel.filtered(lambda pc: channel_dict[pc.channel_id.id].channel_type == 'chat')
+ direct_partners = direct_channel_partners.mapped('partner_id')
+ partner_infos = self.partner_info(all_partners, direct_partners)
+ channel_last_message_ids = dict((r['id'], r['message_id']) for r in self._channel_last_message_ids())
+
+ for channel in self:
+ info = {
+ 'id': channel.id,
+ 'name': channel.name,
+ 'uuid': channel.uuid,
+ 'state': 'open',
+ 'is_minimized': False,
+ 'channel_type': channel.channel_type,
+ 'public': channel.public,
+ 'mass_mailing': channel.email_send,
+ 'moderation': channel.moderation,
+ 'is_moderator': self.env.uid in channel.moderator_ids.ids,
+ 'group_based_subscription': bool(channel.group_ids),
+ 'create_uid': channel.create_uid.id,
+ }
+ if extra_info:
+ info['info'] = extra_info
+
+ # add last message preview (only used in mobile)
+ info['last_message_id'] = channel_last_message_ids.get(channel.id, False)
+ # listeners of the channel
+ channel_partners = all_partner_channel.filtered(lambda pc: channel.id == pc.channel_id.id)
+
+ # find the channel partner state, if logged user
+ if self.env.user and self.env.user.partner_id:
+ # add needaction and unread counter, since the user is logged
+ info['message_needaction_counter'] = channel.message_needaction_counter
+ info['message_unread_counter'] = channel.message_unread_counter
+
+ # add user session state, if available and if user is logged
+ partner_channel = channel_partners.filtered(lambda pc: pc.partner_id.id == self.env.user.partner_id.id)
+ if partner_channel:
+ partner_channel = partner_channel[0]
+ info['state'] = partner_channel.fold_state or 'open'
+ info['is_minimized'] = partner_channel.is_minimized
+ info['seen_message_id'] = partner_channel.seen_message_id.id
+ info['custom_channel_name'] = partner_channel.custom_channel_name
+ info['is_pinned'] = partner_channel.is_pinned
+
+ # add members infos
+ 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
+ # assumed to be smaller and it's important to know the member list for them
+ partner_ids = channel_partners.mapped('partner_id').ids
+ info['members'] = [partner_infos[partner] for partner in partner_ids]
+ if channel.channel_type != 'channel':
+ info['seen_partners_info'] = [{
+ 'id': cp.id,
+ 'partner_id': cp.partner_id.id,
+ 'fetched_message_id': cp.fetched_message_id.id,
+ 'seen_message_id': cp.seen_message_id.id,
+ } for cp in channel_partners]
+
+ channel_infos.append(info)
+ return channel_infos
+
+ def channel_fetch_message(self, last_id=False, limit=20):
+ """ Return message values of the current channel.
+ :param last_id : last message id to start the research
+ :param limit : maximum number of messages to fetch
+ :returns list of messages values
+ :rtype : list(dict)
+ """
+ self.ensure_one()
+ domain = [("channel_ids", "in", self.ids)]
+ if last_id:
+ domain.append(("id", "<", last_id))
+ return self.env['mail.message'].message_fetch(domain=domain, limit=limit)
+
+ # User methods
+ @api.model
+ def channel_get(self, partners_to, pin=True):
+ """ Get the canonical private channel between some partners, create it if needed.
+ To reuse an old channel (conversation), this one must be private, and contains
+ only the given partners.
+ :param partners_to : list of res.partner ids to add to the conversation
+ :param pin : True if getting the channel should pin it for the current user
+ :returns: channel_info of the created or existing channel
+ :rtype: dict
+ """
+ if self.env.user.partner_id.id not in partners_to:
+ partners_to.append(self.env.user.partner_id.id)
+ # determine type according to the number of partner in the channel
+ self.flush()
+ self.env.cr.execute("""
+ SELECT P.channel_id
+ FROM mail_channel C, mail_channel_partner P
+ WHERE P.channel_id = C.id
+ AND C.public LIKE 'private'
+ AND P.partner_id IN %s
+ AND C.channel_type LIKE 'chat'
+ AND NOT EXISTS (
+ SELECT *
+ FROM mail_channel_partner P2
+ WHERE P2.channel_id = C.id
+ AND P2.partner_id NOT IN %s
+ )
+ GROUP BY P.channel_id
+ HAVING ARRAY_AGG(DISTINCT P.partner_id ORDER BY P.partner_id) = %s
+ LIMIT 1
+ """, (tuple(partners_to), tuple(partners_to), sorted(list(partners_to)),))
+ result = self.env.cr.dictfetchall()
+ if result:
+ # get the existing channel between the given partners
+ channel = self.browse(result[0].get('channel_id'))
+ # pin up the channel for the current partner
+ if pin:
+ self.env['mail.channel.partner'].search([('partner_id', '=', self.env.user.partner_id.id), ('channel_id', '=', channel.id)]).write({'is_pinned': True})
+ channel._broadcast(self.env.user.partner_id.ids)
+ else:
+ # create a new one
+ channel = self.create({
+ 'channel_partner_ids': [(4, partner_id) for partner_id in partners_to],
+ 'public': 'private',
+ 'channel_type': 'chat',
+ 'email_send': False,
+ 'name': ', '.join(self.env['res.partner'].sudo().browse(partners_to).mapped('name')),
+ })
+ channel._broadcast(partners_to)
+ return channel.channel_info()[0]
+
+ @api.model
+ def channel_get_and_minimize(self, partners_to):
+ channel = self.channel_get(partners_to)
+ if channel:
+ self.channel_minimize(channel['uuid'])
+ return channel
+
+ @api.model
+ def channel_fold(self, uuid, state=None):
+ """ Update the fold_state of the given session. In order to syncronize web browser
+ tabs, the change will be broadcast to himself (the current user channel).
+ Note: the user need to be logged
+ :param state : the new status of the session for the current user.
+ """
+ domain = [('partner_id', '=', self.env.user.partner_id.id), ('channel_id.uuid', '=', uuid)]
+ for session_state in self.env['mail.channel.partner'].search(domain):
+ if not state:
+ state = session_state.fold_state
+ if session_state.fold_state == 'open':
+ state = 'folded'
+ else:
+ state = 'open'
+ is_minimized = bool(state != 'closed')
+ vals = {}
+ if session_state.fold_state != state:
+ vals['fold_state'] = state
+ if session_state.is_minimized != is_minimized:
+ vals['is_minimized'] = is_minimized
+ if vals:
+ session_state.write(vals)
+ self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), session_state.channel_id.channel_info()[0])
+
+ @api.model
+ def channel_minimize(self, uuid, minimized=True):
+ values = {
+ 'fold_state': minimized and 'open' or 'closed',
+ 'is_minimized': minimized
+ }
+ domain = [('partner_id', '=', self.env.user.partner_id.id), ('channel_id.uuid', '=', uuid)]
+ channel_partners = self.env['mail.channel.partner'].search(domain, limit=1)
+ channel_partners.write(values)
+ self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_partners.channel_id.channel_info()[0])
+
+ @api.model
+ def channel_pin(self, uuid, pinned=False):
+ # add the person in the channel, and pin it (or unpin it)
+ channel = self.search([('uuid', '=', uuid)])
+ channel._execute_channel_pin(pinned)
+
+ def _execute_channel_pin(self, pinned=False):
+ """ Hook for website_livechat channel unpin and cleaning """
+ self.ensure_one()
+ channel_partners = self.env['mail.channel.partner'].search(
+ [('partner_id', '=', self.env.user.partner_id.id), ('channel_id', '=', self.id), ('is_pinned', '!=', pinned)])
+ self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), self.channel_info('unsubscribe' if not pinned else False)[0])
+ if channel_partners:
+ channel_partners.write({'is_pinned': pinned})
+
+ def channel_seen(self, last_message_id=None):
+ """
+ Mark channel as seen by updating seen message id of the current logged partner
+ :param last_message_id: the id of the message to be marked as seen, last message of the
+ thread by default. This param SHOULD be required, the default behaviour is DEPRECATED and
+ kept only for compatibility reasons.
+ """
+ self.ensure_one()
+ domain = [('channel_ids', 'in', self.ids)]
+ if last_message_id:
+ domain = expression.AND([domain, [('id', '<=', last_message_id)]])
+ last_message = self.env['mail.message'].search(domain, order="id DESC", limit=1)
+ if not last_message:
+ return
+
+ self._set_last_seen_message(last_message)
+
+ data = {
+ 'info': 'channel_seen',
+ 'last_message_id': last_message.id,
+ 'partner_id': self.env.user.partner_id.id,
+ }
+ if self.channel_type == 'chat':
+ self.env['bus.bus'].sendmany([[(self._cr.dbname, 'mail.channel', self.id), data]])
+ else:
+ data['channel_id'] = self.id
+ self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), data)
+ return last_message.id
+
+ def _set_last_seen_message(self, last_message):
+ """
+ Set last seen message of `self` channels for the current user.
+ :param last_message: the message to set as last seen message
+ """
+ channel_partner_domain = expression.AND([
+ [('channel_id', 'in', self.ids)],
+ [('partner_id', '=', self.env.user.partner_id.id)],
+ expression.OR([
+ [('seen_message_id', '=', False)],
+ [('seen_message_id', '<', last_message.id)]
+ ])
+ ])
+ channel_partner = self.env['mail.channel.partner'].search(channel_partner_domain)
+ channel_partner.write({
+ 'fetched_message_id': last_message.id,
+ 'seen_message_id': last_message.id,
+ })
+
+ def channel_fetched(self):
+ """ Broadcast the channel_fetched notification to channel members
+ :param channel_ids : list of channel id that has been fetched by current user
+ """
+ for channel in self:
+ if not channel.channel_message_ids.ids:
+ return
+ if channel.channel_type != 'chat':
+ return
+ last_message_id = channel.channel_message_ids.ids[0] # zero is the index of the last message
+ channel_partner = self.env['mail.channel.partner'].search([('channel_id', '=', channel.id), ('partner_id', '=', self.env.user.partner_id.id)], limit=1)
+ if channel_partner.fetched_message_id.id == last_message_id:
+ # last message fetched by user is already up-to-date
+ return
+ channel_partner.write({
+ 'fetched_message_id': last_message_id,
+ })
+ data = {
+ 'id': channel_partner.id,
+ 'info': 'channel_fetched',
+ 'last_message_id': last_message_id,
+ 'partner_id': self.env.user.partner_id.id,
+ }
+ self.env['bus.bus'].sendmany([[(self._cr.dbname, 'mail.channel', channel.id), data]])
+
+ def channel_invite(self, partner_ids):
+ """ Add the given partner_ids to the current channels and broadcast the channel header to them.
+ :param partner_ids : list of partner id to add
+ """
+ partners = self.env['res.partner'].browse(partner_ids)
+ self._invite_check_access(partners)
+
+ # add the partner
+ for channel in self:
+ partners_to_add = partners - channel.channel_partner_ids
+ channel.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': partner_id}) for partner_id in partners_to_add.ids]})
+ for partner in partners_to_add:
+ if partner.id != self.env.user.partner_id.id:
+ notification = _('<div class="o_mail_notification">%(author)s invited %(new_partner)s to <a href="#" class="o_channel_redirect" data-oe-id="%(channel_id)s">#%(channel_name)s</a></div>',
+ author=self.env.user.display_name,
+ new_partner=partner.display_name,
+ channel_id=channel.id,
+ channel_name=channel.name,
+ )
+ else:
+ notification = _('<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>', channel.id, channel.name)
+ self.message_post(body=notification, message_type="notification", subtype_xmlid="mail.mt_comment", author_id=partner.id, notify_by_email=False)
+
+ # broadcast the channel header to the added partner
+ self._broadcast(partner_ids)
+
+ def _invite_check_access(self, partners):
+ """ Check invited partners could match channel access """
+ failed = []
+ if any(channel.public == 'groups' for channel in self):
+ for channel in self.filtered(lambda c: c.public == 'groups'):
+ invalid_partners = [partner for partner in partners if channel.group_public_id not in partner.mapped('user_ids.groups_id')]
+ failed += [(channel, partner) for partner in invalid_partners]
+
+ if failed:
+ raise UserError(
+ _('Following invites are invalid as user groups do not match: %s') %
+ ', '.join('%s (channel %s)' % (partner.name, channel.name) for channel, partner in failed)
+ )
+
+ def _can_invite(self, partner_id):
+ """Return True if the current user can invite the partner to the channel."""
+ self.ensure_one()
+ sudo_self = self.sudo()
+ if sudo_self.public == 'public':
+ return True
+ if sudo_self.public == 'private':
+ return self.is_member
+
+ # get the user related to the invited partner
+ partner = self.env['res.partner'].browse(partner_id).exists()
+ invited_user_id = partner.user_ids[:1]
+ if invited_user_id:
+ return (self.env.user | invited_user_id) <= sudo_self.group_public_id.users
+ return False
+
+ @api.model
+ def channel_set_custom_name(self, channel_id, name=False):
+ domain = [('partner_id', '=', self.env.user.partner_id.id), ('channel_id.id', '=', channel_id)]
+ channel_partners = self.env['mail.channel.partner'].search(domain, limit=1)
+ channel_partners.write({
+ 'custom_channel_name': name,
+ })
+
+ def notify_typing(self, is_typing):
+ """ Broadcast the typing notification to channel members
+ :param is_typing: (boolean) tells whether the current user is typing or not
+ """
+ notifications = []
+ for channel in self:
+ data = {
+ 'info': 'typing_status',
+ 'is_typing': is_typing,
+ 'partner_id': self.env.user.partner_id.id,
+ 'partner_name': self.env.user.partner_id.name,
+ }
+ notifications.append([(self._cr.dbname, 'mail.channel', channel.id), data]) # notify backend users
+ notifications.append([channel.uuid, data]) # notify frontend users
+ self.env['bus.bus'].sendmany(notifications)
+
+ #------------------------------------------------------
+ # Instant Messaging View Specific (Slack Client Action)
+ #------------------------------------------------------
+ @api.model
+ def channel_fetch_slot(self):
+ """ Return the channels of the user grouped by 'slot' (channel, direct_message or private_group), and
+ the mapping between partner_id/channel_id for direct_message channels.
+ :returns dict : the grouped channels and the mapping
+ """
+ values = {}
+ my_partner_id = self.env.user.partner_id.id
+ pinned_channels = self.env['mail.channel.partner'].search([('partner_id', '=', my_partner_id), ('is_pinned', '=', True)]).mapped('channel_id')
+
+ # get the group/public channels
+ values['channel_channel'] = self.search([('channel_type', '=', 'channel'), ('public', 'in', ['public', 'groups']), ('channel_partner_ids', 'in', [my_partner_id])]).channel_info()
+
+ # get the pinned 'direct message' channel
+ direct_message_channels = self.search([('channel_type', '=', 'chat'), ('id', 'in', pinned_channels.ids)])
+ values['channel_direct_message'] = direct_message_channels.channel_info()
+
+ # get the private group
+ values['channel_private_group'] = self.search([('channel_type', '=', 'channel'), ('public', '=', 'private'), ('channel_partner_ids', 'in', [my_partner_id])]).channel_info()
+ return values
+
+ @api.model
+ def channel_search_to_join(self, name=None, domain=None):
+ """ Return the channel info of the channel the current partner can join
+ :param name : the name of the researched channels
+ :param domain : the base domain of the research
+ :returns dict : channel dict
+ """
+ if not domain:
+ domain = []
+ domain = expression.AND([
+ [('channel_type', '=', 'channel')],
+ [('channel_partner_ids', 'not in', [self.env.user.partner_id.id])],
+ [('public', '!=', 'private')],
+ domain
+ ])
+ if name:
+ domain = expression.AND([domain, [('name', 'ilike', '%'+name+'%')]])
+ return self.search(domain).read(['name', 'public', 'uuid', 'channel_type'])
+
+ def channel_join_and_get_info(self):
+ self.ensure_one()
+ added = self.action_follow()
+ if added and self.channel_type == 'channel' and not self.email_send:
+ notification = _('<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>', self.id, self.name)
+ self.message_post(body=notification, message_type="notification", subtype_xmlid="mail.mt_comment")
+
+ if added and self.moderation_guidelines:
+ self._send_guidelines(self.env.user.partner_id)
+
+ channel_info = self.channel_info('join')[0]
+ self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_info)
+ return channel_info
+
+ @api.model
+ def channel_create(self, name, privacy='public'):
+ """ Create a channel and add the current partner, broadcast it (to make the user directly
+ listen to it when polling)
+ :param name : the name of the channel to create
+ :param privacy : privacy of the channel. Should be 'public' or 'private'.
+ :return dict : channel header
+ """
+ # create the channel
+ new_channel = self.create({
+ 'name': name,
+ 'public': privacy,
+ 'email_send': False,
+ })
+ notification = _('<div class="o_mail_notification">created <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>', new_channel.id, new_channel.name)
+ new_channel.message_post(body=notification, message_type="notification", subtype_xmlid="mail.mt_comment")
+ channel_info = new_channel.channel_info('creation')[0]
+ self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_info)
+ return channel_info
+
+ @api.model
+ def get_mention_suggestions(self, search, limit=8):
+ """ Return 'limit'-first channels' id, name and public fields such that the name matches a
+ 'search' string. Exclude channels of type chat (DM), and private channels the current
+ user isn't registered to. """
+ domain = expression.AND([
+ [('name', 'ilike', search)],
+ [('channel_type', '=', 'channel')],
+ expression.OR([
+ [('public', '!=', 'private')],
+ [('channel_partner_ids', 'in', [self.env.user.partner_id.id])]
+ ])
+ ])
+ return self.search_read(domain, ['id', 'name', 'public', 'channel_type'], limit=limit)
+
+ @api.model
+ def channel_fetch_listeners(self, uuid):
+ """ Return the id, name and email of partners listening to the given channel """
+ self._cr.execute("""
+ SELECT P.id, P.name, P.email
+ FROM mail_channel_partner CP
+ INNER JOIN res_partner P ON CP.partner_id = P.id
+ INNER JOIN mail_channel C ON CP.channel_id = C.id
+ WHERE C.uuid = %s""", (uuid,))
+ return self._cr.dictfetchall()
+
+ def channel_fetch_preview(self):
+ """ Return the last message of the given channels """
+ if not self:
+ return []
+ channels_last_message_ids = self._channel_last_message_ids()
+ channels_preview = dict((r['message_id'], r) for r in channels_last_message_ids)
+ last_messages = self.env['mail.message'].browse(channels_preview).message_format()
+ for message in last_messages:
+ channel = channels_preview[message['id']]
+ del(channel['message_id'])
+ channel['last_message'] = message
+ return list(channels_preview.values())
+
+ def _channel_last_message_ids(self):
+ """ Return the last message of the given channels."""
+ if not self:
+ return []
+ self.flush()
+ self.env.cr.execute("""
+ SELECT mail_channel_id AS id, MAX(mail_message_id) AS message_id
+ FROM mail_message_mail_channel_rel
+ WHERE mail_channel_id IN %s
+ GROUP BY mail_channel_id
+ """, (tuple(self.ids),))
+ return self.env.cr.dictfetchall()
+
+ #------------------------------------------------------
+ # Commands
+ #------------------------------------------------------
+ @api.model
+ @ormcache()
+ def get_mention_commands(self):
+ """ Returns the allowed commands in channels """
+ commands = []
+ for n in dir(self):
+ match = re.search('^_define_command_(.+?)$', n)
+ if match:
+ command = getattr(self, n)()
+ command['name'] = match.group(1)
+ commands.append(command)
+ return commands
+
+ def execute_command(self, command='', **kwargs):
+ """ Executes a given command """
+ self.ensure_one()
+ command_callback = getattr(self, '_execute_command_' + command, False)
+ if command_callback:
+ command_callback(**kwargs)
+
+ def _send_transient_message(self, partner_to, content):
+ """ Notifies partner_to that a message (not stored in DB) has been
+ written in this channel """
+ self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', partner_to.id), {
+ 'body': "<span class='o_mail_notification'>" + content + "</span>",
+ 'channel_ids': [self.id],
+ 'info': 'transient_message',
+ })
+
+ def _define_command_help(self):
+ return {'help': _("Show a helper message")}
+
+ def _execute_command_help(self, **kwargs):
+ partner = self.env.user.partner_id
+ if self.channel_type == 'channel':
+ msg = _("You are in channel <b>#%s</b>.", self.name)
+ if self.public == 'private':
+ msg += _(" This channel is private. People must be invited to join it.")
+ else:
+ all_channel_partners = self.env['mail.channel.partner'].with_context(active_test=False)
+ channel_partners = all_channel_partners.search([('partner_id', '!=', partner.id), ('channel_id', '=', self.id)])
+ msg = _("You are in a private conversation with <b>@%s</b>.", channel_partners[0].partner_id.name if channel_partners else _('Anonymous'))
+ msg += _("""<br><br>
+ Type <b>@username</b> to mention someone, and grab his attention.<br>
+ Type <b>#channel</b> to mention a channel.<br>
+ Type <b>/command</b> to execute a command.<br>
+ Type <b>:shortcut</b> to insert a canned response in your message.<br>""")
+
+ self._send_transient_message(partner, msg)
+
+ def _define_command_leave(self):
+ return {'help': _("Leave this channel")}
+
+ def _execute_command_leave(self, **kwargs):
+ if self.channel_type == 'channel':
+ self.action_unfollow()
+ else:
+ self.channel_pin(self.uuid, False)
+
+ def _define_command_who(self):
+ return {
+ 'channel_types': ['channel', 'chat'],
+ 'help': _("List users in the current channel")
+ }
+
+ def _execute_command_who(self, **kwargs):
+ partner = self.env.user.partner_id
+ members = [
+ '<a href="#" data-oe-id='+str(p.id)+' data-oe-model="res.partner">@'+p.name+'</a>'
+ for p in self.channel_partner_ids[:30] if p != partner
+ ]
+ if len(members) == 0:
+ msg = _("You are alone in this channel.")
+ else:
+ dots = "..." if len(members) != len(self.channel_partner_ids) - 1 else ""
+ msg = _("Users in this channel: %(members)s %(dots)s and you.", members=", ".join(members), dots=dots)
+
+ self._send_transient_message(partner, msg)