19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:27 +01:00
parent d1963a3c3a
commit 2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions

View file

@ -0,0 +1,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import channel
from . import gif
from . import public_page
from . import rtc
from . import search
from . import settings
from . import voice

View file

@ -0,0 +1,222 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from markupsafe import Markup
from werkzeug.exceptions import NotFound
from odoo import http
from odoo.http import request
from odoo.addons.mail.controllers.webclient import WebclientController
from odoo.addons.mail.tools.discuss import add_guest_to_context, Store
class DiscussChannelWebclientController(WebclientController):
"""Override to add discuss channel specific features."""
@classmethod
def _process_request_loop(self, store: Store, fetch_params):
"""Override to add discuss channel specific features."""
# aggregate of channels to return, to batch them in a single query when all the fetch params
# have been processed
request.update_context(
channels=request.env["discuss.channel"], add_channels_last_message=False
)
super()._process_request_loop(store, fetch_params)
channels = request.env.context["channels"]
if channels:
store.add(channels)
if request.env.context["add_channels_last_message"]:
# fetch channels data before messages to benefit from prefetching (channel info might
# prefetch a lot of data that message format could use)
store.add(channels._get_last_messages())
@classmethod
def _process_request_for_all(self, store: Store, name, params):
"""Override to return channel as member and last messages."""
super()._process_request_for_all(store, name, params)
if name == "init_messaging":
member_domain = [("is_self", "=", True), ("rtc_inviting_session_id", "!=", False)]
channel_domain = [("channel_member_ids", "any", member_domain)]
channels = request.env["discuss.channel"].search(channel_domain)
request.update_context(channels=request.env.context["channels"] | channels)
if name == "channels_as_member":
channels = request.env["discuss.channel"]._get_channels_as_member()
request.update_context(
channels=request.env.context["channels"] | channels, add_channels_last_message=True
)
if name == "discuss.channel":
channels = request.env["discuss.channel"].search([("id", "in", params)])
request.update_context(channels=request.env.context["channels"] | channels)
if name == "/discuss/get_or_create_chat":
channel = request.env["discuss.channel"]._get_or_create_chat(
params["partners_to"], params.get("pin", True)
)
store.add(channel).resolve_data_request(channel=Store.One(channel, []))
if name == "/discuss/create_channel":
channel = request.env["discuss.channel"]._create_channel(params["name"], params["group_id"])
store.add(channel).resolve_data_request(channel=Store.One(channel, []))
if name == "/discuss/create_group":
channel = request.env["discuss.channel"]._create_group(
params["partners_to"],
params.get("default_display_mode", False),
params.get("name", ""),
)
store.add(channel).resolve_data_request(channel=Store.One(channel, []))
class ChannelController(http.Controller):
@http.route("/discuss/channel/members", methods=["POST"], type="jsonrpc", auth="public", readonly=True)
@add_guest_to_context
def discuss_channel_members(self, channel_id, known_member_ids):
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
if not channel:
raise NotFound()
unknown_members = self.env["discuss.channel.member"].search(
domain=[("id", "not in", known_member_ids), ("channel_id", "=", channel.id)],
limit=100,
)
store = Store().add(channel, "member_count").add(unknown_members)
return store.get_result()
@http.route("/discuss/channel/update_avatar", methods=["POST"], type="jsonrpc")
def discuss_channel_avatar_update(self, channel_id, data):
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
if not channel or not data:
raise NotFound()
channel.write({"image_128": data})
@http.route("/discuss/channel/messages", methods=["POST"], type="jsonrpc", auth="public")
@add_guest_to_context
def discuss_channel_messages(self, channel_id, fetch_params=None):
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
if not channel:
raise NotFound()
res = request.env["mail.message"]._message_fetch(domain=None, thread=channel, **(fetch_params or {}))
messages = res.pop("messages")
if not request.env.user._is_public():
messages.set_message_done()
return {
**res,
"data": Store().add(messages).get_result(),
"messages": messages.ids,
}
@http.route("/discuss/channel/pinned_messages", methods=["POST"], type="jsonrpc", auth="public", readonly=True)
@add_guest_to_context
def discuss_channel_pins(self, channel_id):
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
if not channel:
raise NotFound()
messages = channel.pinned_message_ids.sorted(key="pinned_at", reverse=True)
return Store().add(messages).get_result()
@http.route("/discuss/channel/mark_as_read", methods=["POST"], type="jsonrpc", auth="public")
@add_guest_to_context
def discuss_channel_mark_as_read(self, channel_id, last_message_id):
member = request.env["discuss.channel.member"].search([
("channel_id", "=", channel_id),
("is_self", "=", True),
])
if not member:
return # ignore if the member left in the meantime
member._mark_as_read(last_message_id)
@http.route("/discuss/channel/set_new_message_separator", methods=["POST"], type="jsonrpc", auth="public")
@add_guest_to_context
def discuss_channel_set_new_message_separator(self, channel_id, message_id):
member = request.env["discuss.channel.member"].search([
("channel_id", "=", channel_id),
("is_self", "=", True),
])
if not member:
raise NotFound()
return member._set_new_message_separator(message_id)
@http.route("/discuss/channel/notify_typing", methods=["POST"], type="jsonrpc", auth="public")
@add_guest_to_context
def discuss_channel_notify_typing(self, channel_id, is_typing):
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
if not channel:
raise request.not_found()
if is_typing:
member = channel._find_or_create_member_for_self()
else:
# Do not create member automatically when setting typing to `False`
# as it could be resulting from the user leaving.
member = request.env["discuss.channel.member"].search(
[
("channel_id", "=", channel_id),
("is_self", "=", True),
]
)
if member:
member._notify_typing(is_typing)
@http.route("/discuss/channel/attachments", methods=["POST"], type="jsonrpc", auth="public", readonly=True)
@add_guest_to_context
def load_attachments(self, channel_id, limit=30, before=None):
"""Load attachments of a channel. If before is set, load attachments
older than the given id.
:param channel_id: id of the channel
:param limit: maximum number of attachments to return
:param before: id of the attachment from which to load older attachments
"""
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
if not channel:
raise NotFound()
domain = [
["res_id", "=", channel_id],
["res_model", "=", "discuss.channel"],
]
if before:
domain.append(["id", "<", before])
# sudo: ir.attachment - reading attachments of a channel that the current user can access
attachments = request.env["ir.attachment"].sudo().search(domain, limit=limit, order="id DESC")
return {
"store_data": Store().add(attachments).get_result(),
"count": len(attachments),
}
@http.route("/discuss/channel/join", methods=["POST"], type="jsonrpc", auth="public")
@add_guest_to_context
def discuss_channel_join(self, channel_id):
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
if not channel:
raise NotFound()
channel._find_or_create_member_for_self()
return Store().add(channel).get_result()
@http.route("/discuss/channel/sub_channel/create", methods=["POST"], type="jsonrpc", auth="public")
def discuss_channel_sub_channel_create(self, parent_channel_id, from_message_id=None, name=None):
channel = request.env["discuss.channel"].search([("id", "=", parent_channel_id)])
if not channel:
raise NotFound()
sub_channel = channel._create_sub_channel(from_message_id, name)
return {"store_data": Store().add(sub_channel).get_result(), "sub_channel": sub_channel.id}
@http.route("/discuss/channel/sub_channel/fetch", methods=["POST"], type="jsonrpc", auth="public")
@add_guest_to_context
def discuss_channel_sub_channel_fetch(self, parent_channel_id, search_term=None, before=None, limit=30):
channel = request.env["discuss.channel"].search([("id", "=", parent_channel_id)])
if not channel:
raise NotFound()
domain = [("parent_channel_id", "=", channel.id)]
if before:
domain.append(("id", "<", before))
if search_term:
domain.append(("name", "ilike", search_term))
sub_channels = request.env["discuss.channel"].search(domain, order="id desc", limit=limit)
return {
"store_data": Store().add(sub_channels).add(sub_channels._get_last_messages()).get_result(),
"sub_channel_ids": sub_channels.ids,
}
@http.route("/discuss/channel/sub_channel/delete", methods=["POST"], type="jsonrpc", auth="user")
def discuss_delete_sub_channel(self, sub_channel_id):
channel = request.env["discuss.channel"].search_fetch([("id", "=", sub_channel_id)])
if not channel or not channel.parent_channel_id or channel.create_uid != request.env.user:
raise NotFound()
body = Markup('<div class="o_mail_notification" data-oe-type="thread_deletion">%s</div>') % channel.name
channel.parent_channel_id.message_post(body=body, subtype_xmlid="mail.mt_comment")
# sudo: discuss.channel - skipping ACL for users who created the thread
channel.sudo().unlink()

View file

@ -0,0 +1,104 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import requests
import urllib3
import werkzeug.urls
from werkzeug.exceptions import BadRequest
from odoo.http import request, route, Controller
TENOR_CONTENT_FILTER = "medium"
TENOR_GIF_LIMIT = 8
_logger = logging.getLogger(__name__)
class DiscussGifController(Controller):
def _request_gifs(self, endpoint):
response = None
try:
response = requests.get(
f"https://tenor.googleapis.com/v2/{endpoint}", timeout=3
)
response.raise_for_status()
except (urllib3.exceptions.MaxRetryError, requests.exceptions.HTTPError):
_logger.error("Exceeded the request's maximum size for a searching term.")
if not response:
raise BadRequest()
return response
@route("/discuss/gif/search", type="jsonrpc", auth="user")
def search(self, search_term, locale="en", country="US", position=None, readonly=True):
# sudo: ir.config_parameter - read keys are hard-coded and values are only used for server requests
ir_config = request.env["ir.config_parameter"].sudo()
query_string = werkzeug.urls.url_encode(
{
"q": search_term,
"key": ir_config.get_param("discuss.tenor_api_key"),
"client_key": request.env.cr.dbname,
"limit": TENOR_GIF_LIMIT,
"contentfilter": TENOR_CONTENT_FILTER,
"locale": locale,
"country": country,
"media_filter": "tinygif",
"pos": position,
}
)
response = self._request_gifs(f"search?{query_string}")
if response:
return response.json()
@route("/discuss/gif/categories", type="jsonrpc", auth="user", readonly=True)
def categories(self, locale="en", country="US"):
# sudo: ir.config_parameter - read keys are hard-coded and values are only used for server requests
ir_config = request.env["ir.config_parameter"].sudo()
query_string = werkzeug.urls.url_encode(
{
"key": ir_config.get_param("discuss.tenor_api_key"),
"client_key": request.env.cr.dbname,
"limit": TENOR_GIF_LIMIT,
"contentfilter": TENOR_CONTENT_FILTER,
"locale": locale,
"country": country,
}
)
response = self._request_gifs(f"categories?{query_string}")
if response:
return response.json()
@route("/discuss/gif/add_favorite", type="jsonrpc", auth="user")
def add_favorite(self, tenor_gif_id):
request.env["discuss.gif.favorite"].create({"tenor_gif_id": tenor_gif_id})
def _gif_posts(self, ids):
# sudo: ir.config_parameter - read keys are hard-coded and values are only used for server requests
ir_config = request.env["ir.config_parameter"].sudo()
query_string = werkzeug.urls.url_encode(
{
"ids": ",".join(ids),
"key": ir_config.get_param("discuss.tenor_api_key"),
"client_key": request.env.cr.dbname,
"media_filter": "tinygif",
}
)
response = self._request_gifs(f"posts?{query_string}")
if response:
return response.json()["results"]
@route("/discuss/gif/favorites", type="jsonrpc", auth="user", readonly=True)
def get_favorites(self, offset=0):
tenor_gif_ids = request.env["discuss.gif.favorite"].search(
[("create_uid", "=", request.env.user.id)], limit=20, offset=offset
)
return (self._gif_posts(tenor_gif_ids.mapped("tenor_gif_id")) or [],)
@route("/discuss/gif/remove_favorite", type="jsonrpc", auth="user")
def remove_favorite(self, tenor_gif_id):
request.env["discuss.gif.favorite"].search(
[
("create_uid", "=", request.env.user.id),
("tenor_gif_id", "=", tenor_gif_id),
]
).unlink()

View file

@ -0,0 +1,125 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import psycopg2.errors
from werkzeug.exceptions import NotFound
from odoo import _, http
from odoo.exceptions import UserError
from odoo.http import request
from odoo.tools import consteq, email_normalize, replace_exceptions
from odoo.tools.misc import verify_hash_signed
from odoo.addons.mail.tools.discuss import add_guest_to_context, Store
class PublicPageController(http.Controller):
@http.route(
[
"/chat/<string:create_token>",
"/chat/<string:create_token>/<string:channel_name>",
],
methods=["GET"],
type="http",
auth="public",
)
@add_guest_to_context
def discuss_channel_chat_from_token(self, create_token, channel_name=None):
return self._response_discuss_channel_from_token(create_token=create_token, channel_name=channel_name)
@http.route(
[
"/meet/<string:create_token>",
"/meet/<string:create_token>/<string:channel_name>",
],
methods=["GET"],
type="http",
auth="public",
)
@add_guest_to_context
def discuss_channel_meet_from_token(self, create_token, channel_name=None):
return self._response_discuss_channel_from_token(
create_token=create_token, channel_name=channel_name, default_display_mode="video_full_screen"
)
@http.route("/chat/<int:channel_id>/<string:invitation_token>", methods=["GET"], type="http", auth="public")
@add_guest_to_context
def discuss_channel_invitation(self, channel_id, invitation_token, email_token=None):
guest_email = email_token and verify_hash_signed(
self.env(su=True), "mail.invite_email", email_token
)
guest_email = email_normalize(guest_email)
channel = request.env["discuss.channel"].browse(channel_id).exists()
# sudo: discuss.channel - channel access is validated with invitation_token
if not channel or not channel.sudo().uuid or not consteq(channel.sudo().uuid, invitation_token):
raise NotFound()
store = Store().add_global_values(isChannelTokenSecret=True)
return self._response_discuss_channel_invitation(store, channel, guest_email)
@http.route("/discuss/channel/<int:channel_id>", methods=["GET"], type="http", auth="public")
@add_guest_to_context
def discuss_channel(self, channel_id, *, highlight_message_id=None):
# highlight_message_id is used JS side by parsing the query string
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
if not channel:
raise NotFound()
return self._response_discuss_public_template(Store(), channel)
def _response_discuss_channel_from_token(self, create_token, channel_name=None, default_display_mode=False):
# sudo: ir.config_parameter - reading hard-coded key and using it in a simple condition
if not request.env["ir.config_parameter"].sudo().get_param("mail.chat_from_token"):
raise NotFound()
# sudo: discuss.channel - channel access is validated with invitation_token
channel_sudo = request.env["discuss.channel"].sudo().search([("uuid", "=", create_token)])
if not channel_sudo:
try:
channel_sudo = channel_sudo.create(
{
"channel_type": "channel",
"default_display_mode": default_display_mode,
"group_public_id": None,
"name": channel_name or create_token,
"uuid": create_token,
}
)
except psycopg2.errors.UniqueViolation:
# concurrent insert attempt: another request created the channel.
# commit the current transaction and get the channel.
request.env.cr.commit()
channel_sudo = channel_sudo.search([("uuid", "=", create_token)])
store = Store().add_global_values(isChannelTokenSecret=False)
return self._response_discuss_channel_invitation(store, channel_sudo.sudo(False))
def _response_discuss_channel_invitation(self, store, channel, guest_email=None):
# group restriction takes precedence over token
# sudo - res.groups: can access group public id of parent channel to determine if we
# can access the channel.
group_public_id = channel.group_public_id or channel.parent_channel_id.sudo().group_public_id
if group_public_id and group_public_id not in request.env.user.all_group_ids:
raise request.not_found()
guest_already_known = channel.env["mail.guest"]._get_guest_from_context()
with replace_exceptions(UserError, by=NotFound()):
# sudo: mail.guest - creating a guest and its member inside a channel of which they have the token
__, guest = channel.sudo()._find_or_create_persona_for_channel(
guest_name=guest_email if guest_email else _("Guest"),
country_code=request.geoip.country_code,
timezone=request.env["mail.guest"]._get_timezone_from_request(request),
)
if guest_email and not guest.email:
# sudo - mail.guest: writing email address of self guest is allowed
guest.sudo().email = guest_email
if guest and not guest_already_known:
store.add_global_values(is_welcome_page_displayed=True)
channel = channel.with_context(guest=guest)
return self._response_discuss_public_template(store, channel)
def _response_discuss_public_template(self, store: Store, channel):
store.add_global_values(
companyName=request.env.company.name,
inPublicPage=True,
)
store.add_singleton_values("DiscussApp", {"thread": store.One(channel)})
return request.render(
"mail.discuss_public_channel_template",
{
"data": store.get_result(),
"session_info": channel.env["ir.http"].session_info(),
},
)

View file

@ -0,0 +1,148 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from werkzeug.exceptions import NotFound
from odoo import http
from odoo.http import request
from odoo.tools import file_open
from odoo.addons.mail.tools.discuss import add_guest_to_context, Store
class RtcController(http.Controller):
@http.route("/mail/rtc/session/notify_call_members", methods=["POST"], type="jsonrpc", auth="public")
@add_guest_to_context
def session_call_notify(self, peer_notifications):
"""Sends content to other session of the same channel, only works if the user is the user of that session.
This is used to send peer to peer information between sessions.
:param peer_notifications: list of tuple with the following elements:
- int sender_session_id: id of the session from which the content is sent
- list target_session_ids: list of the ids of the sessions that should receive the content
- string content: the content to send to the other sessions
"""
guest = request.env["mail.guest"]._get_guest_from_context()
notifications_by_session = defaultdict(list)
for sender_session_id, target_session_ids, content in peer_notifications:
# sudo: discuss.channel.rtc.session - only keeping sessions matching the current user
session_sudo = request.env["discuss.channel.rtc.session"].sudo().browse(int(sender_session_id)).exists()
if (
not session_sudo
or (session_sudo.guest_id and session_sudo.guest_id != guest)
or (session_sudo.partner_id and session_sudo.partner_id != request.env.user.partner_id)
):
continue
notifications_by_session[session_sudo].append(([int(sid) for sid in target_session_ids], content))
for session_sudo, notifications in notifications_by_session.items():
session_sudo._notify_peers(notifications)
@http.route("/mail/rtc/session/update_and_broadcast", methods=["POST"], type="jsonrpc", auth="public")
@add_guest_to_context
def session_update_and_broadcast(self, session_id, values):
"""Update a RTC session and broadcasts the changes to the members of its channel,
only works of the user is the user of that session.
:param int session_id: id of the session to update
:param dict values: write dict for the fields to update
"""
if request.env.user._is_public():
guest = request.env["mail.guest"]._get_guest_from_context()
if guest:
# sudo: discuss.channel.rtc.session - only keeping sessions matching the current user
session = guest.env["discuss.channel.rtc.session"].sudo().browse(int(session_id)).exists()
if session and session.guest_id == guest:
session._update_and_broadcast(values)
return
return
# sudo: discuss.channel.rtc.session - only keeping sessions matching the current user
session = request.env["discuss.channel.rtc.session"].sudo().browse(int(session_id)).exists()
if session and session.partner_id == request.env.user.partner_id:
session._update_and_broadcast(values)
@http.route("/mail/rtc/channel/join_call", methods=["POST"], type="jsonrpc", auth="public")
@add_guest_to_context
def channel_call_join(self, channel_id, check_rtc_session_ids=None, camera=False):
"""Joins the RTC call of a channel if the user is a member of that channel
:param int channel_id: id of the channel to join
"""
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
if not channel:
raise request.not_found()
member = channel._find_or_create_member_for_self()
if not member:
raise NotFound()
store = Store()
# sudo: discuss.channel.rtc.session - member of current user can join call
member.sudo()._rtc_join_call(store, check_rtc_session_ids=check_rtc_session_ids, camera=camera)
return store.get_result()
@http.route("/mail/rtc/channel/leave_call", methods=["POST"], type="jsonrpc", auth="public")
@add_guest_to_context
def channel_call_leave(self, channel_id, session_id=None):
"""Disconnects the current user from a rtc call and clears any invitation sent to that user on this channel
:param int channel_id: id of the channel from which to disconnect
:param int session_id: id of the leaving session
"""
member = request.env["discuss.channel.member"].search([("channel_id", "=", channel_id), ("is_self", "=", True)])
if not member:
raise NotFound()
# sudo: discuss.channel.rtc.session - member of current user can leave call
member.sudo()._rtc_leave_call(session_id)
@http.route("/mail/rtc/channel/upgrade_connection", methods=["POST"], type="jsonrpc", auth="user")
def channel_upgrade(self, channel_id):
member = request.env["discuss.channel.member"].search([("channel_id", "=", channel_id), ("is_self", "=", True)])
if not member:
raise NotFound()
member.sudo()._join_sfu(force=True)
@http.route("/mail/rtc/channel/cancel_call_invitation", methods=["POST"], type="jsonrpc", auth="public")
@add_guest_to_context
def channel_call_cancel_invitation(self, channel_id, member_ids=None):
"""
:param member_ids: members whose invitation is to cancel
:type member_ids: list(int) or None
"""
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
if not channel:
raise NotFound()
# sudo: discuss.channel.rtc.session - can cancel invitations in accessible channel
channel.sudo()._rtc_cancel_invitations(member_ids=member_ids)
@http.route("/mail/rtc/audio_worklet_processor_v2", methods=["GET"], type="http", auth="public", readonly=True)
def audio_worklet_processor(self):
"""Returns a JS file that declares a WorkletProcessor class in
a WorkletGlobalScope, which means that it cannot be added to the
bundles like other assets.
"""
with file_open("mail/static/src/worklets/audio_processor.js", "rb") as f:
data = f.read()
return request.make_response(
data,
headers=[
("Content-Type", "application/javascript"),
("Cache-Control", f"max-age={http.STATIC_CACHE}"),
],
)
@http.route("/discuss/channel/ping", methods=["POST"], type="jsonrpc", auth="public")
@add_guest_to_context
def channel_ping(self, channel_id, rtc_session_id=None, check_rtc_session_ids=None):
member = request.env["discuss.channel.member"].search([("channel_id", "=", channel_id), ("is_self", "=", True)])
if not member:
raise NotFound()
# sudo: discuss.channel.rtc.session - member of current user can access related sessions
channel_member_sudo = member.sudo()
if rtc_session_id:
domain = [
("id", "=", int(rtc_session_id)),
("channel_member_id", "=", member.id),
]
channel_member_sudo.channel_id.rtc_session_ids.filtered_domain(domain).write({}) # update write_date
current_rtc_sessions, outdated_rtc_sessions = channel_member_sudo._rtc_sync_sessions(check_rtc_session_ids)
return Store().add(
member.channel_id,
[
{"rtc_session_ids": Store.Many(current_rtc_sessions, mode="ADD")},
{"rtc_session_ids": Store.Many(outdated_rtc_sessions, [], mode="DELETE")},
],
).get_result()

View file

@ -0,0 +1,34 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http
from odoo.fields import Domain
from odoo.http import request
from odoo.addons.mail.tools.discuss import add_guest_to_context, Store
class SearchController(http.Controller):
@http.route("/discuss/search", methods=["POST"], type="jsonrpc", auth="public")
@add_guest_to_context
def search(self, term, category_id=None, limit=10):
store = Store()
self.get_search_store(store, search_term=term, limit=limit)
return store.get_result()
def get_search_store(self, store: Store, search_term, limit):
base_domain = Domain("name", "ilike", search_term) & Domain("channel_type", "!=", "chat")
priority_conditions = [
Domain("is_member", "=", True) & base_domain,
base_domain,
]
channels = self.env["discuss.channel"]
for domain in priority_conditions:
remaining_limit = limit - len(channels)
if remaining_limit <= 0:
break
# We are using _search to avoid the default order that is
# automatically added by the search method. "Order by" makes the query
# really slow.
query = channels._search(Domain('id', 'not in', channels.ids) & domain, limit=remaining_limit)
channels |= channels.browse(query)
store.add(channels)
request.env["res.partner"]._search_for_channel_invite(store, search_term=search_term, limit=limit)

View file

@ -0,0 +1,49 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from dateutil.relativedelta import relativedelta
from odoo import fields
from odoo.http import request, route, Controller
class DiscussSettingsController(Controller):
@route("/discuss/settings/mute", methods=["POST"], type="jsonrpc", auth="user")
def discuss_mute(self, minutes, channel_id):
"""Mute notifications for the given number of minutes.
:param minutes: (integer) number of minutes to mute notifications, -1 means mute until the user unmutes
:param channel_id: (integer) id of the discuss.channel record
"""
channel = request.env["discuss.channel"].browse(channel_id)
if not channel:
raise request.not_found()
member = channel._find_or_create_member_for_self()
if not member:
raise request.not_found()
if minutes == -1:
member.mute_until_dt = datetime.max
elif minutes:
member.mute_until_dt = fields.Datetime.now() + relativedelta(minutes=minutes)
else:
member.mute_until_dt = False
member._notify_mute()
@route("/discuss/settings/custom_notifications", methods=["POST"], type="jsonrpc", auth="user")
def discuss_custom_notifications(self, custom_notifications, channel_id=None):
"""Set custom notifications for the given channel or general user settings.
:param custom_notifications: (false|all|mentions|no_notif) custom notifications to set
:param channel_id: (integer) id of the discuss.channel record, if not set, set for res.users.settings
"""
if channel_id:
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
if not channel:
raise request.not_found()
member = channel._find_or_create_member_for_self()
if not member:
raise request.not_found()
member.custom_notifications = custom_notifications
else:
user_settings = request.env["res.users.settings"]._find_or_create_for_user(request.env.user)
if not user_settings:
raise request.not_found()
user_settings.set_res_users_settings({"channel_notifications": custom_notifications})

View file

@ -0,0 +1,19 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http
from odoo.http import request
from odoo.tools import file_open
class VoiceController(http.Controller):
@http.route("/discuss/voice/worklet_processor", methods=["GET"], type="http", auth="public", readonly=True)
def voice_worklet_processor(self):
with file_open("mail/static/src/discuss/voice_message/worklets/processor.js", "rb") as f:
data = f.read()
return request.make_response(
data,
headers=[
("Content-Type", "application/javascript"),
],
)