mirror of
https://github.com/zulip/zulip.git
synced 2025-11-06 15:03:34 +00:00
views: Extract message_send.py for sending views.
This commit is contained in:
@@ -114,7 +114,7 @@ from zerver.models import (
|
|||||||
get_system_bot,
|
get_system_bot,
|
||||||
get_user,
|
get_user,
|
||||||
)
|
)
|
||||||
from zerver.views.messages import InvalidMirrorInput, create_mirrored_message_users
|
from zerver.views.message_send import InvalidMirrorInput, create_mirrored_message_users
|
||||||
|
|
||||||
|
|
||||||
class MiscMessageTest(ZulipTestCase):
|
class MiscMessageTest(ZulipTestCase):
|
||||||
@@ -2233,7 +2233,7 @@ class MessagePOSTTest(ZulipTestCase):
|
|||||||
subdomain="zephyr")
|
subdomain="zephyr")
|
||||||
self.assert_json_error(result, "User not authorized for this query")
|
self.assert_json_error(result, "User not authorized for this query")
|
||||||
|
|
||||||
@mock.patch("zerver.views.messages.create_mirrored_message_users")
|
@mock.patch("zerver.views.message_send.create_mirrored_message_users")
|
||||||
def test_send_message_create_mirrored_message_user_returns_invalid_input(
|
def test_send_message_create_mirrored_message_user_returns_invalid_input(
|
||||||
self, create_mirrored_message_users_mock: Any) -> None:
|
self, create_mirrored_message_users_mock: Any) -> None:
|
||||||
create_mirrored_message_users_mock.side_effect = InvalidMirrorInput()
|
create_mirrored_message_users_mock.side_effect = InvalidMirrorInput()
|
||||||
@@ -2246,7 +2246,7 @@ class MessagePOSTTest(ZulipTestCase):
|
|||||||
subdomain="zephyr")
|
subdomain="zephyr")
|
||||||
self.assert_json_error(result, "Invalid mirrored message")
|
self.assert_json_error(result, "Invalid mirrored message")
|
||||||
|
|
||||||
@mock.patch("zerver.views.messages.create_mirrored_message_users")
|
@mock.patch("zerver.views.message_send.create_mirrored_message_users")
|
||||||
def test_send_message_when_client_is_zephyr_mirror_but_string_id_is_not_zephyr(
|
def test_send_message_when_client_is_zephyr_mirror_but_string_id_is_not_zephyr(
|
||||||
self, create_mirrored_message_users_mock: Any) -> None:
|
self, create_mirrored_message_users_mock: Any) -> None:
|
||||||
create_mirrored_message_users_mock.return_value = mock.Mock()
|
create_mirrored_message_users_mock.return_value = mock.Mock()
|
||||||
@@ -2262,7 +2262,7 @@ class MessagePOSTTest(ZulipTestCase):
|
|||||||
subdomain="notzephyr")
|
subdomain="notzephyr")
|
||||||
self.assert_json_error(result, "Zephyr mirroring is not allowed in this organization")
|
self.assert_json_error(result, "Zephyr mirroring is not allowed in this organization")
|
||||||
|
|
||||||
@mock.patch("zerver.views.messages.create_mirrored_message_users")
|
@mock.patch("zerver.views.message_send.create_mirrored_message_users")
|
||||||
def test_send_message_when_client_is_zephyr_mirror_but_recipient_is_user_id(
|
def test_send_message_when_client_is_zephyr_mirror_but_recipient_is_user_id(
|
||||||
self, create_mirrored_message_users_mock: Any) -> None:
|
self, create_mirrored_message_users_mock: Any) -> None:
|
||||||
create_mirrored_message_users_mock.return_value = mock.Mock()
|
create_mirrored_message_users_mock.return_value = mock.Mock()
|
||||||
|
|||||||
300
zerver/views/message_send.py
Normal file
300
zerver/views/message_send.py
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
from typing import Iterable, Optional, Sequence, Union, cast
|
||||||
|
|
||||||
|
from dateutil.parser import parse as dateparser
|
||||||
|
from django.core import validators
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django.utils.timezone import now as timezone_now
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
from zerver.decorator import REQ, has_request_variables
|
||||||
|
from zerver.lib.actions import (
|
||||||
|
check_schedule_message,
|
||||||
|
check_send_message,
|
||||||
|
compute_irc_user_fullname,
|
||||||
|
compute_jabber_user_fullname,
|
||||||
|
create_mirror_user_if_needed,
|
||||||
|
extract_private_recipients,
|
||||||
|
extract_stream_indicator,
|
||||||
|
)
|
||||||
|
from zerver.lib.message import render_markdown
|
||||||
|
from zerver.lib.response import json_error, json_success
|
||||||
|
from zerver.lib.timestamp import convert_to_UTC
|
||||||
|
from zerver.lib.timezone import get_timezone
|
||||||
|
from zerver.lib.topic import REQ_topic
|
||||||
|
from zerver.lib.zcommand import process_zcommands
|
||||||
|
from zerver.lib.zephyr import compute_mit_user_fullname
|
||||||
|
from zerver.models import (
|
||||||
|
Client,
|
||||||
|
Message,
|
||||||
|
Realm,
|
||||||
|
RealmDomain,
|
||||||
|
UserProfile,
|
||||||
|
email_to_domain,
|
||||||
|
get_realm,
|
||||||
|
get_user_including_cross_realm,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidMirrorInput(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create_mirrored_message_users(request: HttpRequest, user_profile: UserProfile,
|
||||||
|
recipients: Iterable[str]) -> UserProfile:
|
||||||
|
if "sender" not in request.POST:
|
||||||
|
raise InvalidMirrorInput("No sender")
|
||||||
|
|
||||||
|
sender_email = request.POST["sender"].strip().lower()
|
||||||
|
referenced_users = {sender_email}
|
||||||
|
if request.POST['type'] == 'private':
|
||||||
|
for email in recipients:
|
||||||
|
referenced_users.add(email.lower())
|
||||||
|
|
||||||
|
if request.client.name == "zephyr_mirror":
|
||||||
|
user_check = same_realm_zephyr_user
|
||||||
|
fullname_function = compute_mit_user_fullname
|
||||||
|
elif request.client.name == "irc_mirror":
|
||||||
|
user_check = same_realm_irc_user
|
||||||
|
fullname_function = compute_irc_user_fullname
|
||||||
|
elif request.client.name in ("jabber_mirror", "JabberMirror"):
|
||||||
|
user_check = same_realm_jabber_user
|
||||||
|
fullname_function = compute_jabber_user_fullname
|
||||||
|
else:
|
||||||
|
raise InvalidMirrorInput("Unrecognized mirroring client")
|
||||||
|
|
||||||
|
for email in referenced_users:
|
||||||
|
# Check that all referenced users are in our realm:
|
||||||
|
if not user_check(user_profile, email):
|
||||||
|
raise InvalidMirrorInput("At least one user cannot be mirrored")
|
||||||
|
|
||||||
|
# Create users for the referenced users, if needed.
|
||||||
|
for email in referenced_users:
|
||||||
|
create_mirror_user_if_needed(user_profile.realm, email, fullname_function)
|
||||||
|
|
||||||
|
sender = get_user_including_cross_realm(sender_email, user_profile.realm)
|
||||||
|
return sender
|
||||||
|
|
||||||
|
def same_realm_zephyr_user(user_profile: UserProfile, email: str) -> bool:
|
||||||
|
#
|
||||||
|
# Are the sender and recipient both addresses in the same Zephyr
|
||||||
|
# mirroring realm? We have to handle this specially, inferring
|
||||||
|
# the domain from the e-mail address, because the recipient may
|
||||||
|
# not existing in Zulip and we may need to make a stub Zephyr
|
||||||
|
# mirroring user on the fly.
|
||||||
|
try:
|
||||||
|
validators.validate_email(email)
|
||||||
|
except ValidationError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
domain = email_to_domain(email)
|
||||||
|
|
||||||
|
# Assumes allow_subdomains=False for all RealmDomain's corresponding to
|
||||||
|
# these realms.
|
||||||
|
return user_profile.realm.is_zephyr_mirror_realm and \
|
||||||
|
RealmDomain.objects.filter(realm=user_profile.realm, domain=domain).exists()
|
||||||
|
|
||||||
|
def same_realm_irc_user(user_profile: UserProfile, email: str) -> bool:
|
||||||
|
# Check whether the target email address is an IRC user in the
|
||||||
|
# same realm as user_profile, i.e. if the domain were example.com,
|
||||||
|
# the IRC user would need to be username@irc.example.com
|
||||||
|
try:
|
||||||
|
validators.validate_email(email)
|
||||||
|
except ValidationError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
domain = email_to_domain(email).replace("irc.", "")
|
||||||
|
|
||||||
|
# Assumes allow_subdomains=False for all RealmDomain's corresponding to
|
||||||
|
# these realms.
|
||||||
|
return RealmDomain.objects.filter(realm=user_profile.realm, domain=domain).exists()
|
||||||
|
|
||||||
|
def same_realm_jabber_user(user_profile: UserProfile, email: str) -> bool:
|
||||||
|
try:
|
||||||
|
validators.validate_email(email)
|
||||||
|
except ValidationError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# If your Jabber users have a different email domain than the
|
||||||
|
# Zulip users, this is where you would do any translation.
|
||||||
|
domain = email_to_domain(email)
|
||||||
|
|
||||||
|
# Assumes allow_subdomains=False for all RealmDomain's corresponding to
|
||||||
|
# these realms.
|
||||||
|
return RealmDomain.objects.filter(realm=user_profile.realm, domain=domain).exists()
|
||||||
|
|
||||||
|
def handle_deferred_message(sender: UserProfile, client: Client,
|
||||||
|
message_type_name: str,
|
||||||
|
message_to: Union[Sequence[str], Sequence[int]],
|
||||||
|
topic_name: Optional[str],
|
||||||
|
message_content: str, delivery_type: str,
|
||||||
|
defer_until: str, tz_guess: Optional[str],
|
||||||
|
forwarder_user_profile: UserProfile,
|
||||||
|
realm: Optional[Realm]) -> HttpResponse:
|
||||||
|
deliver_at = None
|
||||||
|
local_tz = 'UTC'
|
||||||
|
if tz_guess:
|
||||||
|
local_tz = tz_guess
|
||||||
|
elif sender.timezone:
|
||||||
|
local_tz = sender.timezone
|
||||||
|
try:
|
||||||
|
deliver_at = dateparser(defer_until)
|
||||||
|
except ValueError:
|
||||||
|
return json_error(_("Invalid time format"))
|
||||||
|
|
||||||
|
deliver_at_usertz = deliver_at
|
||||||
|
if deliver_at_usertz.tzinfo is None:
|
||||||
|
user_tz = get_timezone(local_tz)
|
||||||
|
# Since mypy is not able to recognize localize and normalize as attributes of tzinfo we use ignore.
|
||||||
|
deliver_at_usertz = user_tz.normalize(user_tz.localize(deliver_at)) # type: ignore[attr-defined] # Reason in comment on previous line.
|
||||||
|
deliver_at = convert_to_UTC(deliver_at_usertz)
|
||||||
|
|
||||||
|
if deliver_at <= timezone_now():
|
||||||
|
return json_error(_("Time must be in the future."))
|
||||||
|
|
||||||
|
check_schedule_message(sender, client, message_type_name, message_to,
|
||||||
|
topic_name, message_content, delivery_type,
|
||||||
|
deliver_at, realm=realm,
|
||||||
|
forwarder_user_profile=forwarder_user_profile)
|
||||||
|
return json_success({"deliver_at": str(deliver_at_usertz)})
|
||||||
|
|
||||||
|
@has_request_variables
|
||||||
|
def send_message_backend(request: HttpRequest, user_profile: UserProfile,
|
||||||
|
message_type_name: str=REQ('type'),
|
||||||
|
req_to: Optional[str]=REQ('to', default=None),
|
||||||
|
forged_str: Optional[str]=REQ("forged",
|
||||||
|
default=None,
|
||||||
|
documentation_pending=True),
|
||||||
|
topic_name: Optional[str]=REQ_topic(),
|
||||||
|
message_content: str=REQ('content'),
|
||||||
|
widget_content: Optional[str]=REQ(default=None,
|
||||||
|
documentation_pending=True),
|
||||||
|
realm_str: Optional[str]=REQ('realm_str', default=None,
|
||||||
|
documentation_pending=True),
|
||||||
|
local_id: Optional[str]=REQ(default=None,
|
||||||
|
documentation_pending=True),
|
||||||
|
queue_id: Optional[str]=REQ(default=None,
|
||||||
|
documentation_pending=True),
|
||||||
|
delivery_type: str=REQ('delivery_type', default='send_now',
|
||||||
|
documentation_pending=True),
|
||||||
|
defer_until: Optional[str]=REQ('deliver_at', default=None,
|
||||||
|
documentation_pending=True),
|
||||||
|
tz_guess: Optional[str]=REQ('tz_guess', default=None,
|
||||||
|
documentation_pending=True),
|
||||||
|
) -> HttpResponse:
|
||||||
|
|
||||||
|
# If req_to is None, then we default to an
|
||||||
|
# empty list of recipients.
|
||||||
|
message_to: Union[Sequence[int], Sequence[str]] = []
|
||||||
|
|
||||||
|
if req_to is not None:
|
||||||
|
if message_type_name == 'stream':
|
||||||
|
stream_indicator = extract_stream_indicator(req_to)
|
||||||
|
|
||||||
|
# For legacy reasons check_send_message expects
|
||||||
|
# a list of streams, instead of a single stream.
|
||||||
|
#
|
||||||
|
# Also, mypy can't detect that a single-item
|
||||||
|
# list populated from a Union[int, str] is actually
|
||||||
|
# a Union[Sequence[int], Sequence[str]].
|
||||||
|
message_to = cast(
|
||||||
|
Union[Sequence[int], Sequence[str]],
|
||||||
|
[stream_indicator],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
message_to = extract_private_recipients(req_to)
|
||||||
|
|
||||||
|
# Temporary hack: We're transitioning `forged` from accepting
|
||||||
|
# `yes` to accepting `true` like all of our normal booleans.
|
||||||
|
forged = forged_str is not None and forged_str in ["yes", "true"]
|
||||||
|
|
||||||
|
client = request.client
|
||||||
|
is_super_user = request.user.is_api_super_user
|
||||||
|
if forged and not is_super_user:
|
||||||
|
return json_error(_("User not authorized for this query"))
|
||||||
|
|
||||||
|
realm = None
|
||||||
|
if realm_str and realm_str != user_profile.realm.string_id:
|
||||||
|
if not is_super_user:
|
||||||
|
# The email gateway bot needs to be able to send messages in
|
||||||
|
# any realm.
|
||||||
|
return json_error(_("User not authorized for this query"))
|
||||||
|
try:
|
||||||
|
realm = get_realm(realm_str)
|
||||||
|
except Realm.DoesNotExist:
|
||||||
|
return json_error(_("Unknown organization '{}'").format(realm_str))
|
||||||
|
|
||||||
|
if client.name in ["zephyr_mirror", "irc_mirror", "jabber_mirror", "JabberMirror"]:
|
||||||
|
# Here's how security works for mirroring:
|
||||||
|
#
|
||||||
|
# For private messages, the message must be (1) both sent and
|
||||||
|
# received exclusively by users in your realm, and (2)
|
||||||
|
# received by the forwarding user.
|
||||||
|
#
|
||||||
|
# For stream messages, the message must be (1) being forwarded
|
||||||
|
# by an API superuser for your realm and (2) being sent to a
|
||||||
|
# mirrored stream.
|
||||||
|
#
|
||||||
|
# The most important security checks are in
|
||||||
|
# `create_mirrored_message_users` below, which checks the
|
||||||
|
# same-realm constraint.
|
||||||
|
if "sender" not in request.POST:
|
||||||
|
return json_error(_("Missing sender"))
|
||||||
|
if message_type_name != "private" and not is_super_user:
|
||||||
|
return json_error(_("User not authorized for this query"))
|
||||||
|
|
||||||
|
# For now, mirroring only works with recipient emails, not for
|
||||||
|
# recipient user IDs.
|
||||||
|
if not all(isinstance(to_item, str) for to_item in message_to):
|
||||||
|
return json_error(_("Mirroring not allowed with recipient user IDs"))
|
||||||
|
|
||||||
|
# We need this manual cast so that mypy doesn't complain about
|
||||||
|
# create_mirrored_message_users not being able to accept a Sequence[int]
|
||||||
|
# type parameter.
|
||||||
|
message_to = cast(Sequence[str], message_to)
|
||||||
|
|
||||||
|
try:
|
||||||
|
mirror_sender = create_mirrored_message_users(request, user_profile, message_to)
|
||||||
|
except InvalidMirrorInput:
|
||||||
|
return json_error(_("Invalid mirrored message"))
|
||||||
|
|
||||||
|
if client.name == "zephyr_mirror" and not user_profile.realm.is_zephyr_mirror_realm:
|
||||||
|
return json_error(_("Zephyr mirroring is not allowed in this organization"))
|
||||||
|
sender = mirror_sender
|
||||||
|
else:
|
||||||
|
if "sender" in request.POST:
|
||||||
|
return json_error(_("Invalid mirrored message"))
|
||||||
|
sender = user_profile
|
||||||
|
|
||||||
|
if (delivery_type == 'send_later' or delivery_type == 'remind') and defer_until is None:
|
||||||
|
return json_error(_("Missing deliver_at in a request for delayed message delivery"))
|
||||||
|
|
||||||
|
if (delivery_type == 'send_later' or delivery_type == 'remind') and defer_until is not None:
|
||||||
|
return handle_deferred_message(sender, client, message_type_name,
|
||||||
|
message_to, topic_name, message_content,
|
||||||
|
delivery_type, defer_until, tz_guess,
|
||||||
|
forwarder_user_profile=user_profile,
|
||||||
|
realm=realm)
|
||||||
|
|
||||||
|
ret = check_send_message(sender, client, message_type_name, message_to,
|
||||||
|
topic_name, message_content, forged=forged,
|
||||||
|
forged_timestamp = request.POST.get('time'),
|
||||||
|
forwarder_user_profile=user_profile, realm=realm,
|
||||||
|
local_id=local_id, sender_queue_id=queue_id,
|
||||||
|
widget_content=widget_content)
|
||||||
|
return json_success({"id": ret})
|
||||||
|
|
||||||
|
@has_request_variables
|
||||||
|
def zcommand_backend(request: HttpRequest, user_profile: UserProfile,
|
||||||
|
command: str=REQ('command')) -> HttpResponse:
|
||||||
|
return json_success(process_zcommands(command, user_profile))
|
||||||
|
|
||||||
|
@has_request_variables
|
||||||
|
def render_message_backend(request: HttpRequest, user_profile: UserProfile,
|
||||||
|
content: str=REQ()) -> HttpResponse:
|
||||||
|
message = Message()
|
||||||
|
message.sender = user_profile
|
||||||
|
message.content = content
|
||||||
|
message.sending_client = request.client
|
||||||
|
|
||||||
|
rendered_content = render_markdown(message, content, realm=user_profile.realm)
|
||||||
|
return json_success({"rendered": rendered_content})
|
||||||
@@ -1,15 +1,12 @@
|
|||||||
import re
|
import re
|
||||||
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union, cast
|
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
||||||
|
|
||||||
import ujson
|
import ujson
|
||||||
from dateutil.parser import parse as dateparser
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import validators
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.utils.html import escape as escape_html
|
from django.utils.html import escape as escape_html
|
||||||
from django.utils.timezone import now as timezone_now
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
from sqlalchemy.dialects import postgresql
|
from sqlalchemy.dialects import postgresql
|
||||||
@@ -30,19 +27,10 @@ from sqlalchemy.sql import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from zerver.decorator import REQ, has_request_variables
|
from zerver.decorator import REQ, has_request_variables
|
||||||
from zerver.lib.actions import (
|
from zerver.lib.actions import recipient_for_user_profiles
|
||||||
check_schedule_message,
|
|
||||||
check_send_message,
|
|
||||||
compute_irc_user_fullname,
|
|
||||||
compute_jabber_user_fullname,
|
|
||||||
create_mirror_user_if_needed,
|
|
||||||
extract_private_recipients,
|
|
||||||
extract_stream_indicator,
|
|
||||||
recipient_for_user_profiles,
|
|
||||||
)
|
|
||||||
from zerver.lib.addressee import get_user_profiles, get_user_profiles_by_ids
|
from zerver.lib.addressee import get_user_profiles, get_user_profiles_by_ids
|
||||||
from zerver.lib.exceptions import ErrorCode, JsonableError
|
from zerver.lib.exceptions import ErrorCode, JsonableError
|
||||||
from zerver.lib.message import get_first_visible_message_id, messages_for_ids, render_markdown
|
from zerver.lib.message import get_first_visible_message_id, messages_for_ids
|
||||||
from zerver.lib.response import json_error, json_success
|
from zerver.lib.response import json_error, json_success
|
||||||
from zerver.lib.sqlalchemy_utils import get_sqlalchemy_connection
|
from zerver.lib.sqlalchemy_utils import get_sqlalchemy_connection
|
||||||
from zerver.lib.streams import (
|
from zerver.lib.streams import (
|
||||||
@@ -51,9 +39,7 @@ from zerver.lib.streams import (
|
|||||||
get_public_streams_queryset,
|
get_public_streams_queryset,
|
||||||
get_stream_by_narrow_operand_access_unchecked,
|
get_stream_by_narrow_operand_access_unchecked,
|
||||||
)
|
)
|
||||||
from zerver.lib.timestamp import convert_to_UTC
|
from zerver.lib.topic import DB_TOPIC_NAME, MATCH_TOPIC, topic_column_sa, topic_match_sa
|
||||||
from zerver.lib.timezone import get_timezone
|
|
||||||
from zerver.lib.topic import DB_TOPIC_NAME, MATCH_TOPIC, REQ_topic, topic_column_sa, topic_match_sa
|
|
||||||
from zerver.lib.topic_mutes import exclude_topic_mutes
|
from zerver.lib.topic_mutes import exclude_topic_mutes
|
||||||
from zerver.lib.utils import statsd
|
from zerver.lib.utils import statsd
|
||||||
from zerver.lib.validator import (
|
from zerver.lib.validator import (
|
||||||
@@ -67,21 +53,14 @@ from zerver.lib.validator import (
|
|||||||
check_string_or_int_list,
|
check_string_or_int_list,
|
||||||
to_non_negative_int,
|
to_non_negative_int,
|
||||||
)
|
)
|
||||||
from zerver.lib.zcommand import process_zcommands
|
|
||||||
from zerver.lib.zephyr import compute_mit_user_fullname
|
|
||||||
from zerver.models import (
|
from zerver.models import (
|
||||||
Client,
|
|
||||||
Message,
|
|
||||||
Realm,
|
Realm,
|
||||||
RealmDomain,
|
|
||||||
Recipient,
|
Recipient,
|
||||||
Stream,
|
Stream,
|
||||||
Subscription,
|
Subscription,
|
||||||
UserMessage,
|
UserMessage,
|
||||||
UserProfile,
|
UserProfile,
|
||||||
email_to_domain,
|
|
||||||
get_active_streams,
|
get_active_streams,
|
||||||
get_realm,
|
|
||||||
get_user_by_id_in_realm_including_cross_realm,
|
get_user_by_id_in_realm_including_cross_realm,
|
||||||
get_user_including_cross_realm,
|
get_user_including_cross_realm,
|
||||||
)
|
)
|
||||||
@@ -770,11 +749,6 @@ def find_first_unread_anchor(sa_conn: Any,
|
|||||||
|
|
||||||
return anchor
|
return anchor
|
||||||
|
|
||||||
@has_request_variables
|
|
||||||
def zcommand_backend(request: HttpRequest, user_profile: UserProfile,
|
|
||||||
command: str=REQ('command')) -> HttpResponse:
|
|
||||||
return json_success(process_zcommands(command, user_profile))
|
|
||||||
|
|
||||||
def parse_anchor_value(anchor_val: Optional[str],
|
def parse_anchor_value(anchor_val: Optional[str],
|
||||||
use_first_unread_anchor: bool) -> Optional[int]:
|
use_first_unread_anchor: bool) -> Optional[int]:
|
||||||
"""Given the anchor and use_first_unread_anchor parameters passed by
|
"""Given the anchor and use_first_unread_anchor parameters passed by
|
||||||
@@ -1131,265 +1105,6 @@ def post_process_limited_query(rows: List[Any],
|
|||||||
found_oldest=found_oldest,
|
found_oldest=found_oldest,
|
||||||
history_limited=history_limited,
|
history_limited=history_limited,
|
||||||
)
|
)
|
||||||
|
|
||||||
class InvalidMirrorInput(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def create_mirrored_message_users(request: HttpRequest, user_profile: UserProfile,
|
|
||||||
recipients: Iterable[str]) -> UserProfile:
|
|
||||||
if "sender" not in request.POST:
|
|
||||||
raise InvalidMirrorInput("No sender")
|
|
||||||
|
|
||||||
sender_email = request.POST["sender"].strip().lower()
|
|
||||||
referenced_users = {sender_email}
|
|
||||||
if request.POST['type'] == 'private':
|
|
||||||
for email in recipients:
|
|
||||||
referenced_users.add(email.lower())
|
|
||||||
|
|
||||||
if request.client.name == "zephyr_mirror":
|
|
||||||
user_check = same_realm_zephyr_user
|
|
||||||
fullname_function = compute_mit_user_fullname
|
|
||||||
elif request.client.name == "irc_mirror":
|
|
||||||
user_check = same_realm_irc_user
|
|
||||||
fullname_function = compute_irc_user_fullname
|
|
||||||
elif request.client.name in ("jabber_mirror", "JabberMirror"):
|
|
||||||
user_check = same_realm_jabber_user
|
|
||||||
fullname_function = compute_jabber_user_fullname
|
|
||||||
else:
|
|
||||||
raise InvalidMirrorInput("Unrecognized mirroring client")
|
|
||||||
|
|
||||||
for email in referenced_users:
|
|
||||||
# Check that all referenced users are in our realm:
|
|
||||||
if not user_check(user_profile, email):
|
|
||||||
raise InvalidMirrorInput("At least one user cannot be mirrored")
|
|
||||||
|
|
||||||
# Create users for the referenced users, if needed.
|
|
||||||
for email in referenced_users:
|
|
||||||
create_mirror_user_if_needed(user_profile.realm, email, fullname_function)
|
|
||||||
|
|
||||||
sender = get_user_including_cross_realm(sender_email, user_profile.realm)
|
|
||||||
return sender
|
|
||||||
|
|
||||||
def same_realm_zephyr_user(user_profile: UserProfile, email: str) -> bool:
|
|
||||||
#
|
|
||||||
# Are the sender and recipient both addresses in the same Zephyr
|
|
||||||
# mirroring realm? We have to handle this specially, inferring
|
|
||||||
# the domain from the e-mail address, because the recipient may
|
|
||||||
# not existing in Zulip and we may need to make a stub Zephyr
|
|
||||||
# mirroring user on the fly.
|
|
||||||
try:
|
|
||||||
validators.validate_email(email)
|
|
||||||
except ValidationError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
domain = email_to_domain(email)
|
|
||||||
|
|
||||||
# Assumes allow_subdomains=False for all RealmDomain's corresponding to
|
|
||||||
# these realms.
|
|
||||||
return user_profile.realm.is_zephyr_mirror_realm and \
|
|
||||||
RealmDomain.objects.filter(realm=user_profile.realm, domain=domain).exists()
|
|
||||||
|
|
||||||
def same_realm_irc_user(user_profile: UserProfile, email: str) -> bool:
|
|
||||||
# Check whether the target email address is an IRC user in the
|
|
||||||
# same realm as user_profile, i.e. if the domain were example.com,
|
|
||||||
# the IRC user would need to be username@irc.example.com
|
|
||||||
try:
|
|
||||||
validators.validate_email(email)
|
|
||||||
except ValidationError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
domain = email_to_domain(email).replace("irc.", "")
|
|
||||||
|
|
||||||
# Assumes allow_subdomains=False for all RealmDomain's corresponding to
|
|
||||||
# these realms.
|
|
||||||
return RealmDomain.objects.filter(realm=user_profile.realm, domain=domain).exists()
|
|
||||||
|
|
||||||
def same_realm_jabber_user(user_profile: UserProfile, email: str) -> bool:
|
|
||||||
try:
|
|
||||||
validators.validate_email(email)
|
|
||||||
except ValidationError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# If your Jabber users have a different email domain than the
|
|
||||||
# Zulip users, this is where you would do any translation.
|
|
||||||
domain = email_to_domain(email)
|
|
||||||
|
|
||||||
# Assumes allow_subdomains=False for all RealmDomain's corresponding to
|
|
||||||
# these realms.
|
|
||||||
return RealmDomain.objects.filter(realm=user_profile.realm, domain=domain).exists()
|
|
||||||
|
|
||||||
def handle_deferred_message(sender: UserProfile, client: Client,
|
|
||||||
message_type_name: str,
|
|
||||||
message_to: Union[Sequence[str], Sequence[int]],
|
|
||||||
topic_name: Optional[str],
|
|
||||||
message_content: str, delivery_type: str,
|
|
||||||
defer_until: str, tz_guess: Optional[str],
|
|
||||||
forwarder_user_profile: UserProfile,
|
|
||||||
realm: Optional[Realm]) -> HttpResponse:
|
|
||||||
deliver_at = None
|
|
||||||
local_tz = 'UTC'
|
|
||||||
if tz_guess:
|
|
||||||
local_tz = tz_guess
|
|
||||||
elif sender.timezone:
|
|
||||||
local_tz = sender.timezone
|
|
||||||
try:
|
|
||||||
deliver_at = dateparser(defer_until)
|
|
||||||
except ValueError:
|
|
||||||
return json_error(_("Invalid time format"))
|
|
||||||
|
|
||||||
deliver_at_usertz = deliver_at
|
|
||||||
if deliver_at_usertz.tzinfo is None:
|
|
||||||
user_tz = get_timezone(local_tz)
|
|
||||||
# Since mypy is not able to recognize localize and normalize as attributes of tzinfo we use ignore.
|
|
||||||
deliver_at_usertz = user_tz.normalize(user_tz.localize(deliver_at)) # type: ignore[attr-defined] # Reason in comment on previous line.
|
|
||||||
deliver_at = convert_to_UTC(deliver_at_usertz)
|
|
||||||
|
|
||||||
if deliver_at <= timezone_now():
|
|
||||||
return json_error(_("Time must be in the future."))
|
|
||||||
|
|
||||||
check_schedule_message(sender, client, message_type_name, message_to,
|
|
||||||
topic_name, message_content, delivery_type,
|
|
||||||
deliver_at, realm=realm,
|
|
||||||
forwarder_user_profile=forwarder_user_profile)
|
|
||||||
return json_success({"deliver_at": str(deliver_at_usertz)})
|
|
||||||
|
|
||||||
@has_request_variables
|
|
||||||
def send_message_backend(request: HttpRequest, user_profile: UserProfile,
|
|
||||||
message_type_name: str=REQ('type'),
|
|
||||||
req_to: Optional[str]=REQ('to', default=None),
|
|
||||||
forged_str: Optional[str]=REQ("forged",
|
|
||||||
default=None,
|
|
||||||
documentation_pending=True),
|
|
||||||
topic_name: Optional[str]=REQ_topic(),
|
|
||||||
message_content: str=REQ('content'),
|
|
||||||
widget_content: Optional[str]=REQ(default=None,
|
|
||||||
documentation_pending=True),
|
|
||||||
realm_str: Optional[str]=REQ('realm_str', default=None,
|
|
||||||
documentation_pending=True),
|
|
||||||
local_id: Optional[str]=REQ(default=None,
|
|
||||||
documentation_pending=True),
|
|
||||||
queue_id: Optional[str]=REQ(default=None,
|
|
||||||
documentation_pending=True),
|
|
||||||
delivery_type: str=REQ('delivery_type', default='send_now',
|
|
||||||
documentation_pending=True),
|
|
||||||
defer_until: Optional[str]=REQ('deliver_at', default=None,
|
|
||||||
documentation_pending=True),
|
|
||||||
tz_guess: Optional[str]=REQ('tz_guess', default=None,
|
|
||||||
documentation_pending=True),
|
|
||||||
) -> HttpResponse:
|
|
||||||
|
|
||||||
# If req_to is None, then we default to an
|
|
||||||
# empty list of recipients.
|
|
||||||
message_to: Union[Sequence[int], Sequence[str]] = []
|
|
||||||
|
|
||||||
if req_to is not None:
|
|
||||||
if message_type_name == 'stream':
|
|
||||||
stream_indicator = extract_stream_indicator(req_to)
|
|
||||||
|
|
||||||
# For legacy reasons check_send_message expects
|
|
||||||
# a list of streams, instead of a single stream.
|
|
||||||
#
|
|
||||||
# Also, mypy can't detect that a single-item
|
|
||||||
# list populated from a Union[int, str] is actually
|
|
||||||
# a Union[Sequence[int], Sequence[str]].
|
|
||||||
message_to = cast(
|
|
||||||
Union[Sequence[int], Sequence[str]],
|
|
||||||
[stream_indicator],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
message_to = extract_private_recipients(req_to)
|
|
||||||
|
|
||||||
# Temporary hack: We're transitioning `forged` from accepting
|
|
||||||
# `yes` to accepting `true` like all of our normal booleans.
|
|
||||||
forged = forged_str is not None and forged_str in ["yes", "true"]
|
|
||||||
|
|
||||||
client = request.client
|
|
||||||
is_super_user = request.user.is_api_super_user
|
|
||||||
if forged and not is_super_user:
|
|
||||||
return json_error(_("User not authorized for this query"))
|
|
||||||
|
|
||||||
realm = None
|
|
||||||
if realm_str and realm_str != user_profile.realm.string_id:
|
|
||||||
if not is_super_user:
|
|
||||||
# The email gateway bot needs to be able to send messages in
|
|
||||||
# any realm.
|
|
||||||
return json_error(_("User not authorized for this query"))
|
|
||||||
try:
|
|
||||||
realm = get_realm(realm_str)
|
|
||||||
except Realm.DoesNotExist:
|
|
||||||
return json_error(_("Unknown organization '{}'").format(realm_str))
|
|
||||||
|
|
||||||
if client.name in ["zephyr_mirror", "irc_mirror", "jabber_mirror", "JabberMirror"]:
|
|
||||||
# Here's how security works for mirroring:
|
|
||||||
#
|
|
||||||
# For private messages, the message must be (1) both sent and
|
|
||||||
# received exclusively by users in your realm, and (2)
|
|
||||||
# received by the forwarding user.
|
|
||||||
#
|
|
||||||
# For stream messages, the message must be (1) being forwarded
|
|
||||||
# by an API superuser for your realm and (2) being sent to a
|
|
||||||
# mirrored stream.
|
|
||||||
#
|
|
||||||
# The most important security checks are in
|
|
||||||
# `create_mirrored_message_users` below, which checks the
|
|
||||||
# same-realm constraint.
|
|
||||||
if "sender" not in request.POST:
|
|
||||||
return json_error(_("Missing sender"))
|
|
||||||
if message_type_name != "private" and not is_super_user:
|
|
||||||
return json_error(_("User not authorized for this query"))
|
|
||||||
|
|
||||||
# For now, mirroring only works with recipient emails, not for
|
|
||||||
# recipient user IDs.
|
|
||||||
if not all(isinstance(to_item, str) for to_item in message_to):
|
|
||||||
return json_error(_("Mirroring not allowed with recipient user IDs"))
|
|
||||||
|
|
||||||
# We need this manual cast so that mypy doesn't complain about
|
|
||||||
# create_mirrored_message_users not being able to accept a Sequence[int]
|
|
||||||
# type parameter.
|
|
||||||
message_to = cast(Sequence[str], message_to)
|
|
||||||
|
|
||||||
try:
|
|
||||||
mirror_sender = create_mirrored_message_users(request, user_profile, message_to)
|
|
||||||
except InvalidMirrorInput:
|
|
||||||
return json_error(_("Invalid mirrored message"))
|
|
||||||
|
|
||||||
if client.name == "zephyr_mirror" and not user_profile.realm.is_zephyr_mirror_realm:
|
|
||||||
return json_error(_("Zephyr mirroring is not allowed in this organization"))
|
|
||||||
sender = mirror_sender
|
|
||||||
else:
|
|
||||||
if "sender" in request.POST:
|
|
||||||
return json_error(_("Invalid mirrored message"))
|
|
||||||
sender = user_profile
|
|
||||||
|
|
||||||
if (delivery_type == 'send_later' or delivery_type == 'remind') and defer_until is None:
|
|
||||||
return json_error(_("Missing deliver_at in a request for delayed message delivery"))
|
|
||||||
|
|
||||||
if (delivery_type == 'send_later' or delivery_type == 'remind') and defer_until is not None:
|
|
||||||
return handle_deferred_message(sender, client, message_type_name,
|
|
||||||
message_to, topic_name, message_content,
|
|
||||||
delivery_type, defer_until, tz_guess,
|
|
||||||
forwarder_user_profile=user_profile,
|
|
||||||
realm=realm)
|
|
||||||
|
|
||||||
ret = check_send_message(sender, client, message_type_name, message_to,
|
|
||||||
topic_name, message_content, forged=forged,
|
|
||||||
forged_timestamp = request.POST.get('time'),
|
|
||||||
forwarder_user_profile=user_profile, realm=realm,
|
|
||||||
local_id=local_id, sender_queue_id=queue_id,
|
|
||||||
widget_content=widget_content)
|
|
||||||
return json_success({"id": ret})
|
|
||||||
|
|
||||||
@has_request_variables
|
|
||||||
def render_message_backend(request: HttpRequest, user_profile: UserProfile,
|
|
||||||
content: str=REQ()) -> HttpResponse:
|
|
||||||
message = Message()
|
|
||||||
message.sender = user_profile
|
|
||||||
message.content = content
|
|
||||||
message.sending_client = request.client
|
|
||||||
|
|
||||||
rendered_content = render_markdown(message, content, realm=user_profile.realm)
|
|
||||||
return json_success({"rendered": rendered_content})
|
|
||||||
|
|
||||||
@has_request_variables
|
@has_request_variables
|
||||||
def messages_in_narrow_backend(request: HttpRequest, user_profile: UserProfile,
|
def messages_in_narrow_backend(request: HttpRequest, user_profile: UserProfile,
|
||||||
msg_ids: List[int]=REQ(validator=check_list(check_int)),
|
msg_ids: List[int]=REQ(validator=check_list(check_int)),
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import zerver.views.email_mirror
|
|||||||
import zerver.views.home
|
import zerver.views.home
|
||||||
import zerver.views.message_edit
|
import zerver.views.message_edit
|
||||||
import zerver.views.message_flags
|
import zerver.views.message_flags
|
||||||
|
import zerver.views.message_send
|
||||||
import zerver.views.messages
|
import zerver.views.messages
|
||||||
import zerver.views.muting
|
import zerver.views.muting
|
||||||
import zerver.views.portico
|
import zerver.views.portico
|
||||||
@@ -189,20 +190,20 @@ v1_api_and_json_patterns = [
|
|||||||
{'POST': 'zerver.views.message_flags.mark_topic_as_read'}),
|
{'POST': 'zerver.views.message_flags.mark_topic_as_read'}),
|
||||||
|
|
||||||
url(r'^zcommand$', rest_dispatch,
|
url(r'^zcommand$', rest_dispatch,
|
||||||
{'POST': 'zerver.views.messages.zcommand_backend'}),
|
{'POST': 'zerver.views.message_send.zcommand_backend'}),
|
||||||
|
|
||||||
# messages -> zerver.views.message*
|
# messages -> zerver.views.message*
|
||||||
# GET returns messages, possibly filtered, POST sends a message
|
# GET returns messages, possibly filtered, POST sends a message
|
||||||
url(r'^messages$', rest_dispatch,
|
url(r'^messages$', rest_dispatch,
|
||||||
{'GET': 'zerver.views.messages.get_messages_backend',
|
{'GET': 'zerver.views.messages.get_messages_backend',
|
||||||
'POST': ('zerver.views.messages.send_message_backend',
|
'POST': ('zerver.views.message_send.send_message_backend',
|
||||||
{'allow_incoming_webhooks'})}),
|
{'allow_incoming_webhooks'})}),
|
||||||
url(r'^messages/(?P<message_id>[0-9]+)$', rest_dispatch,
|
url(r'^messages/(?P<message_id>[0-9]+)$', rest_dispatch,
|
||||||
{'GET': 'zerver.views.message_edit.json_fetch_raw_message',
|
{'GET': 'zerver.views.message_edit.json_fetch_raw_message',
|
||||||
'PATCH': 'zerver.views.message_edit.update_message_backend',
|
'PATCH': 'zerver.views.message_edit.update_message_backend',
|
||||||
'DELETE': 'zerver.views.message_edit.delete_message_backend'}),
|
'DELETE': 'zerver.views.message_edit.delete_message_backend'}),
|
||||||
url(r'^messages/render$', rest_dispatch,
|
url(r'^messages/render$', rest_dispatch,
|
||||||
{'POST': 'zerver.views.messages.render_message_backend'}),
|
{'POST': 'zerver.views.message_send.render_message_backend'}),
|
||||||
url(r'^messages/flags$', rest_dispatch,
|
url(r'^messages/flags$', rest_dispatch,
|
||||||
{'POST': 'zerver.views.message_flags.update_message_flags'}),
|
{'POST': 'zerver.views.message_flags.update_message_flags'}),
|
||||||
url(r'^messages/(?P<message_id>\d+)/history$', rest_dispatch,
|
url(r'^messages/(?P<message_id>\d+)/history$', rest_dispatch,
|
||||||
|
|||||||
Reference in New Issue
Block a user