mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 22:32:02 +02:00
19.0 vanilla
This commit is contained in:
parent
d1963a3c3a
commit
2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions
|
|
@ -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
|
||||
222
odoo-bringout-oca-ocb-mail/mail/controllers/discuss/channel.py
Normal file
222
odoo-bringout-oca-ocb-mail/mail/controllers/discuss/channel.py
Normal 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()
|
||||
104
odoo-bringout-oca-ocb-mail/mail/controllers/discuss/gif.py
Normal file
104
odoo-bringout-oca-ocb-mail/mail/controllers/discuss/gif.py
Normal 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()
|
||||
|
|
@ -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(),
|
||||
},
|
||||
)
|
||||
148
odoo-bringout-oca-ocb-mail/mail/controllers/discuss/rtc.py
Normal file
148
odoo-bringout-oca-ocb-mail/mail/controllers/discuss/rtc.py
Normal 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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -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})
|
||||
19
odoo-bringout-oca-ocb-mail/mail/controllers/discuss/voice.py
Normal file
19
odoo-bringout-oca-ocb-mail/mail/controllers/discuss/voice.py
Normal 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"),
|
||||
],
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue