2017-04-27 22:48:06 +02:00
|
|
|
|
2018-08-16 21:45:10 +02:00
|
|
|
from typing import Iterable, List, Optional, Sequence, Union, cast
|
2017-04-27 22:48:06 +02:00
|
|
|
|
|
|
|
from django.utils.translation import ugettext as _
|
2017-08-18 05:01:22 +02:00
|
|
|
from zerver.lib.exceptions import JsonableError
|
2017-08-18 02:15:51 +02:00
|
|
|
from zerver.models import (
|
2017-09-25 23:20:44 +02:00
|
|
|
Realm,
|
2017-08-18 02:15:51 +02:00
|
|
|
UserProfile,
|
2017-08-18 12:26:43 +02:00
|
|
|
get_user_including_cross_realm,
|
2018-08-16 20:54:49 +02:00
|
|
|
get_user_by_id_in_realm_including_cross_realm,
|
2019-02-07 02:05:34 +01:00
|
|
|
Stream,
|
2017-08-18 02:15:51 +02:00
|
|
|
)
|
|
|
|
|
narrow: Handle spurious emails in pm-with searches.
If cordelia searches on pm-with:iago@zulip.com,cordelia@zulip.com,
we now properly treat that the same way as pm-with:iago@zulip.com.
Before this fix, the query would initially go through the
huddle code path. The symptom wasn't completely obvious, as
eventually a deeper function would return a recipient id
corresponding to a single PM with @iago@zulip.com, but we would
only get messages where iago was the recipient, and not any
messages where he was the sender to cordelia.
I put the helper function for this in zerver/lib/addressee, which
is somewhat speculative. Eventually, we'll want pm-with queries
to allow for user ids, and I imagine there will be some shared
logic with other Addressee code in terms of how we handle these
strings. The way we deal with lists of emails/users for various
endpoints is kind of haphazard in the current code, although
granted it's mostly just repeating the same simple patterns. It
would be nice for some of this code to converge a bit. This
affects new messages, typing indicators, search filters, etc.,
and some endpoints have strange legacy stuff like supporting
JSON-encoded lists, so it's not trivial to clean this up.
Tweaked by tabbott to add some additional tests.
2018-10-12 17:56:46 +02:00
|
|
|
def raw_pm_with_emails(email_str: str, my_email: str) -> List[str]:
|
|
|
|
frags = email_str.split(',')
|
|
|
|
emails = [s.strip().lower() for s in frags]
|
|
|
|
emails = [email for email in emails if email]
|
|
|
|
|
|
|
|
if len(emails) > 1:
|
|
|
|
emails = [email for email in emails if email != my_email.lower()]
|
|
|
|
|
|
|
|
return emails
|
|
|
|
|
2018-08-21 01:59:28 +02:00
|
|
|
def get_user_profiles(emails: Iterable[str], realm: Realm) -> List[UserProfile]:
|
2017-08-18 02:15:51 +02:00
|
|
|
user_profiles = [] # type: List[UserProfile]
|
|
|
|
for email in emails:
|
|
|
|
try:
|
2017-09-25 21:51:47 +02:00
|
|
|
user_profile = get_user_including_cross_realm(email, realm)
|
2017-08-18 02:15:51 +02:00
|
|
|
except UserProfile.DoesNotExist:
|
2018-08-21 01:59:28 +02:00
|
|
|
raise JsonableError(_("Invalid email '%s'") % (email,))
|
2017-08-18 02:15:51 +02:00
|
|
|
user_profiles.append(user_profile)
|
|
|
|
return user_profiles
|
2017-04-27 22:48:06 +02:00
|
|
|
|
2018-08-16 20:54:49 +02:00
|
|
|
def get_user_profiles_by_ids(user_ids: Iterable[int], realm: Realm) -> List[UserProfile]:
|
|
|
|
user_profiles = [] # type: List[UserProfile]
|
|
|
|
for user_id in user_ids:
|
|
|
|
try:
|
|
|
|
user_profile = get_user_by_id_in_realm_including_cross_realm(user_id, realm)
|
|
|
|
except UserProfile.DoesNotExist:
|
2019-04-20 03:49:03 +02:00
|
|
|
raise JsonableError(_("Invalid user ID {}").format(user_id))
|
2018-08-16 20:54:49 +02:00
|
|
|
user_profiles.append(user_profile)
|
|
|
|
return user_profiles
|
|
|
|
|
2019-01-26 03:16:16 +01:00
|
|
|
def validate_topic(topic: str) -> str:
|
2019-03-20 22:53:26 +01:00
|
|
|
assert topic is not None
|
2019-01-26 03:16:16 +01:00
|
|
|
topic = topic.strip()
|
|
|
|
if topic == "":
|
|
|
|
raise JsonableError(_("Topic can't be empty"))
|
|
|
|
|
|
|
|
return topic
|
|
|
|
|
2017-11-05 11:37:41 +01:00
|
|
|
class Addressee:
|
2017-04-27 22:48:06 +02:00
|
|
|
# This is really just a holder for vars that tended to be passed
|
|
|
|
# around in a non-type-safe way before this class was introduced.
|
|
|
|
#
|
|
|
|
# It also avoids some nonsense where you have to think about whether
|
|
|
|
# topic should be None or '' for a PM, or you have to make an array
|
|
|
|
# of one stream.
|
|
|
|
#
|
|
|
|
# Eventually we can use this to cache Stream and UserProfile objects
|
|
|
|
# in memory.
|
|
|
|
#
|
|
|
|
# This should be treated as an immutable class.
|
2017-11-05 11:15:10 +01:00
|
|
|
def __init__(self, msg_type: str,
|
|
|
|
user_profiles: Optional[Sequence[UserProfile]]=None,
|
2019-02-07 02:05:34 +01:00
|
|
|
stream: Optional[Stream]=None,
|
2018-05-10 19:13:36 +02:00
|
|
|
stream_name: Optional[str]=None,
|
2019-01-26 03:39:02 +01:00
|
|
|
stream_id: Optional[int]=None,
|
2018-05-10 19:13:36 +02:00
|
|
|
topic: Optional[str]=None) -> None:
|
2017-04-27 22:48:06 +02:00
|
|
|
assert(msg_type in ['stream', 'private'])
|
2019-03-20 22:53:26 +01:00
|
|
|
if msg_type == 'stream' and topic is None:
|
|
|
|
raise JsonableError(_("Missing topic"))
|
2017-04-27 22:48:06 +02:00
|
|
|
self._msg_type = msg_type
|
2017-08-18 05:01:22 +02:00
|
|
|
self._user_profiles = user_profiles
|
2019-02-07 02:05:34 +01:00
|
|
|
self._stream = stream
|
2017-04-27 22:48:06 +02:00
|
|
|
self._stream_name = stream_name
|
2019-01-26 03:39:02 +01:00
|
|
|
self._stream_id = stream_id
|
2017-04-27 22:48:06 +02:00
|
|
|
self._topic = topic
|
|
|
|
|
2017-11-05 11:15:10 +01:00
|
|
|
def is_stream(self) -> bool:
|
2017-04-27 22:48:06 +02:00
|
|
|
return self._msg_type == 'stream'
|
|
|
|
|
2017-11-05 11:15:10 +01:00
|
|
|
def is_private(self) -> bool:
|
2017-04-27 22:48:06 +02:00
|
|
|
return self._msg_type == 'private'
|
|
|
|
|
2017-11-05 11:15:10 +01:00
|
|
|
def user_profiles(self) -> List[UserProfile]:
|
2017-04-27 22:48:06 +02:00
|
|
|
assert(self.is_private())
|
2017-08-18 05:01:22 +02:00
|
|
|
return self._user_profiles # type: ignore # assertion protects us
|
2017-04-27 22:48:06 +02:00
|
|
|
|
2019-02-07 02:05:34 +01:00
|
|
|
def stream(self) -> Optional[Stream]:
|
|
|
|
assert(self.is_stream())
|
|
|
|
return self._stream
|
|
|
|
|
2019-01-28 05:28:29 +01:00
|
|
|
def stream_name(self) -> Optional[str]:
|
2017-04-27 22:48:06 +02:00
|
|
|
assert(self.is_stream())
|
|
|
|
return self._stream_name
|
|
|
|
|
2019-01-26 03:39:02 +01:00
|
|
|
def stream_id(self) -> Optional[int]:
|
|
|
|
assert(self.is_stream())
|
|
|
|
return self._stream_id
|
|
|
|
|
2018-05-10 19:13:36 +02:00
|
|
|
def topic(self) -> str:
|
2017-04-27 22:48:06 +02:00
|
|
|
assert(self.is_stream())
|
2017-10-03 22:33:30 +02:00
|
|
|
assert(self._topic is not None)
|
2017-04-27 22:48:06 +02:00
|
|
|
return self._topic
|
|
|
|
|
|
|
|
@staticmethod
|
2017-11-05 11:15:10 +01:00
|
|
|
def legacy_build(sender: UserProfile,
|
2018-05-10 19:13:36 +02:00
|
|
|
message_type_name: str,
|
2018-08-16 21:45:10 +02:00
|
|
|
message_to: Union[Sequence[int], Sequence[str]],
|
2019-03-20 22:53:26 +01:00
|
|
|
topic_name: Optional[str],
|
2017-11-05 11:15:10 +01:00
|
|
|
realm: Optional[Realm]=None) -> 'Addressee':
|
2017-04-27 22:48:06 +02:00
|
|
|
|
|
|
|
# For legacy reason message_to used to be either a list of
|
|
|
|
# emails or a list of streams. We haven't fixed all of our
|
|
|
|
# callers yet.
|
2017-09-25 21:55:02 +02:00
|
|
|
if realm is None:
|
|
|
|
realm = sender.realm
|
|
|
|
|
2017-04-27 22:48:06 +02:00
|
|
|
if message_type_name == 'stream':
|
|
|
|
if len(message_to) > 1:
|
|
|
|
raise JsonableError(_("Cannot send to multiple streams"))
|
|
|
|
|
|
|
|
if message_to:
|
2019-01-26 03:39:02 +01:00
|
|
|
stream_name_or_id = message_to[0]
|
2017-04-27 22:48:06 +02:00
|
|
|
else:
|
|
|
|
# This is a hack to deal with the fact that we still support
|
|
|
|
# default streams (and the None will be converted later in the
|
|
|
|
# callpath).
|
2017-09-28 21:14:08 +02:00
|
|
|
if sender.default_sending_stream:
|
|
|
|
# Use the users default stream
|
2019-01-30 03:54:28 +01:00
|
|
|
stream_name_or_id = sender.default_sending_stream.id
|
2017-09-28 21:14:08 +02:00
|
|
|
else:
|
|
|
|
raise JsonableError(_('Missing stream'))
|
2017-04-27 22:48:06 +02:00
|
|
|
|
2019-03-20 22:53:26 +01:00
|
|
|
if topic_name is None:
|
|
|
|
raise JsonableError(_("Missing topic"))
|
|
|
|
|
2019-01-26 03:39:02 +01:00
|
|
|
if isinstance(stream_name_or_id, int):
|
|
|
|
stream_id = cast(int, stream_name_or_id)
|
|
|
|
return Addressee.for_stream_id(stream_id, topic_name)
|
|
|
|
|
|
|
|
stream_name = cast(str, stream_name_or_id)
|
2019-02-06 00:20:44 +01:00
|
|
|
return Addressee.for_stream_name(stream_name, topic_name)
|
2017-04-27 22:48:06 +02:00
|
|
|
elif message_type_name == 'private':
|
2018-08-25 00:24:46 +02:00
|
|
|
if not message_to:
|
|
|
|
raise JsonableError(_("Message must have recipients"))
|
|
|
|
|
2018-08-16 21:45:10 +02:00
|
|
|
if isinstance(message_to[0], str):
|
|
|
|
emails = cast(Sequence[str], message_to)
|
|
|
|
return Addressee.for_private(emails, realm)
|
|
|
|
elif isinstance(message_to[0], int):
|
|
|
|
user_ids = cast(Sequence[int], message_to)
|
|
|
|
return Addressee.for_user_ids(user_ids=user_ids, realm=realm)
|
2017-04-27 22:48:06 +02:00
|
|
|
else:
|
|
|
|
raise JsonableError(_("Invalid message type"))
|
|
|
|
|
2019-02-07 02:05:34 +01:00
|
|
|
@staticmethod
|
|
|
|
def for_stream(stream: Stream, topic: str) -> 'Addressee':
|
|
|
|
topic = validate_topic(topic)
|
|
|
|
return Addressee(
|
|
|
|
msg_type='stream',
|
|
|
|
stream=stream,
|
|
|
|
topic=topic,
|
|
|
|
)
|
|
|
|
|
2017-04-27 22:48:06 +02:00
|
|
|
@staticmethod
|
2019-02-06 00:20:44 +01:00
|
|
|
def for_stream_name(stream_name: str, topic: str) -> 'Addressee':
|
2019-01-26 03:16:16 +01:00
|
|
|
topic = validate_topic(topic)
|
2017-04-27 22:48:06 +02:00
|
|
|
return Addressee(
|
|
|
|
msg_type='stream',
|
|
|
|
stream_name=stream_name,
|
|
|
|
topic=topic,
|
|
|
|
)
|
|
|
|
|
2019-01-26 03:39:02 +01:00
|
|
|
@staticmethod
|
|
|
|
def for_stream_id(stream_id: int, topic: str) -> 'Addressee':
|
|
|
|
topic = validate_topic(topic)
|
|
|
|
return Addressee(
|
|
|
|
msg_type='stream',
|
|
|
|
stream_id=stream_id,
|
|
|
|
topic=topic,
|
|
|
|
)
|
|
|
|
|
2017-04-27 22:48:06 +02:00
|
|
|
@staticmethod
|
2018-05-10 19:13:36 +02:00
|
|
|
def for_private(emails: Sequence[str], realm: Realm) -> 'Addressee':
|
2018-08-25 00:24:46 +02:00
|
|
|
assert len(emails) > 0
|
2017-09-25 21:52:55 +02:00
|
|
|
user_profiles = get_user_profiles(emails, realm)
|
2017-04-27 22:48:06 +02:00
|
|
|
return Addressee(
|
|
|
|
msg_type='private',
|
2017-08-18 05:01:22 +02:00
|
|
|
user_profiles=user_profiles,
|
2017-04-27 22:48:06 +02:00
|
|
|
)
|
|
|
|
|
2018-08-16 20:54:49 +02:00
|
|
|
@staticmethod
|
|
|
|
def for_user_ids(user_ids: Sequence[int], realm: Realm) -> 'Addressee':
|
2018-08-25 00:24:46 +02:00
|
|
|
assert len(user_ids) > 0
|
2018-08-16 20:54:49 +02:00
|
|
|
user_profiles = get_user_profiles_by_ids(user_ids, realm)
|
|
|
|
return Addressee(
|
|
|
|
msg_type='private',
|
|
|
|
user_profiles=user_profiles,
|
|
|
|
)
|
|
|
|
|
2017-04-27 22:48:06 +02:00
|
|
|
@staticmethod
|
2017-11-05 11:15:10 +01:00
|
|
|
def for_user_profile(user_profile: UserProfile) -> 'Addressee':
|
2017-08-18 05:02:02 +02:00
|
|
|
user_profiles = [user_profile]
|
2017-04-27 22:48:06 +02:00
|
|
|
return Addressee(
|
|
|
|
msg_type='private',
|
2017-08-18 05:01:22 +02:00
|
|
|
user_profiles=user_profiles,
|
2017-04-27 22:48:06 +02:00
|
|
|
)
|