messages: Add support for passing user IDs for pm-with clause.

We also document support for user IDs in the pm-with narrow operator.

Edited by tabbott to document on /api rather than in the /help page.

Fixes part of #9474.
This commit is contained in:
Priyank Patel 2019-06-08 21:21:01 +00:00 committed by Tim Abbott
parent 33e798168a
commit d7b4de2348
4 changed files with 61 additions and 13 deletions

View File

@ -39,3 +39,7 @@ as a list of simple objects, as follows:
The full set of search/narrowing options supported by the Zulip API is The full set of search/narrowing options supported by the Zulip API is
documented in documented in
[the Zulip Help Center article on search](/help/search-for-messages). [the Zulip Help Center article on search](/help/search-for-messages).
There are a few additional options that we don't document there
because they are primarily useful to API clients:
* `pm-with:1234`: Search 1-on-1 messages by user ID `1234`.

View File

@ -21,6 +21,15 @@ def raw_pm_with_emails(email_str: str, my_email: str) -> List[str]:
return emails return emails
def raw_pm_with_emails_by_ids(user_ids: Iterable[int], my_email: str,
realm: Realm) -> List[str]:
user_profiles = get_user_profiles_by_ids(user_ids, realm)
emails = [user_profile.email for user_profile in user_profiles]
if len(emails) > 1:
emails = [email for email in emails if email != my_email.lower()]
return emails
def get_user_profiles(emails: Iterable[str], realm: Realm) -> List[UserProfile]: def get_user_profiles(emails: Iterable[str], realm: Realm) -> List[UserProfile]:
user_profiles = [] # type: List[UserProfile] user_profiles = [] # type: List[UserProfile]
for email in emails: for email in emails:

View File

@ -1072,6 +1072,10 @@ class GetOldMessagesTest(ZulipTestCase):
assert isinstance(dr, list) assert isinstance(dr, list)
return ','.join(sorted(set([r['email'] for r in dr] + [me]))) return ','.join(sorted(set([r['email'] for r in dr] + [me])))
def dr_ids(dr: Union[str, List[Dict[str, Any]]]) -> List[int]:
assert isinstance(dr, list)
return list(sorted(set([r['id'] for r in dr] + [self.example_user('hamlet').id])))
self.send_personal_message(me, self.example_email("iago")) self.send_personal_message(me, self.example_email("iago"))
self.send_huddle_message( self.send_huddle_message(
me, me,
@ -1081,9 +1085,16 @@ class GetOldMessagesTest(ZulipTestCase):
if not m.is_stream_message()] if not m.is_stream_message()]
for personal in personals: for personal in personals:
emails = dr_emails(get_display_recipient(personal.recipient)) emails = dr_emails(get_display_recipient(personal.recipient))
self.login(me) self.login(me)
narrow = [dict(operator='pm-with', operand=emails)] narrow = [dict(operator='pm-with', operand=emails)] # type: List[Dict[str, Any]]
result = self.get_and_check_messages(dict(narrow=ujson.dumps(narrow)))
for message in result["messages"]:
self.assertEqual(dr_emails(message['display_recipient']), emails)
# check passing id is conistent with passing emails as operand
ids = dr_ids(get_display_recipient(personal.recipient))
narrow = [dict(operator='pm-with', operand=ids)]
result = self.get_and_check_messages(dict(narrow=ujson.dumps(narrow))) result = self.get_and_check_messages(dict(narrow=ujson.dumps(narrow)))
for message in result["messages"]: for message in result["messages"]:
@ -2119,6 +2130,11 @@ class GetOldMessagesTest(ZulipTestCase):
self.exercise_bad_narrow_operand("pm-with", ['non-existent-user@zulip.com'], self.exercise_bad_narrow_operand("pm-with", ['non-existent-user@zulip.com'],
"Invalid narrow operator: unknown user") "Invalid narrow operator: unknown user")
def test_bad_narrow_pm_with_id_list(self) -> None:
self.login(self.example_email('hamlet'))
self.exercise_bad_narrow_operand('pm-with', [-24],
"Bad value for 'narrow': [[\"pm-with\",-24]]")
def test_message_without_rendered_content(self) -> None: def test_message_without_rendered_content(self) -> None:
"""Older messages may not have rendered_content in the database""" """Older messages may not have rendered_content in the database"""
m = self.get_last_message() m = self.get_last_message()

View File

@ -20,7 +20,7 @@ from zerver.lib.actions import recipient_for_emails, do_update_message_flags, \
extract_recipients, truncate_body, render_incoming_message, do_delete_messages, \ extract_recipients, truncate_body, render_incoming_message, do_delete_messages, \
do_mark_all_as_read, do_mark_stream_messages_as_read, \ do_mark_all_as_read, do_mark_stream_messages_as_read, \
get_user_info_for_message_updates, check_schedule_message get_user_info_for_message_updates, check_schedule_message
from zerver.lib.addressee import raw_pm_with_emails from zerver.lib.addressee import raw_pm_with_emails, raw_pm_with_emails_by_ids
from zerver.lib.queue import queue_json_publish from zerver.lib.queue import queue_json_publish
from zerver.lib.message import ( from zerver.lib.message import (
access_message, access_message,
@ -45,7 +45,7 @@ from zerver.lib.topic import (
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 \
check_list, check_int, check_dict, check_string, check_bool check_list, check_int, check_dict, check_string, check_bool, check_string_or_int_list
from zerver.lib.zephyr import compute_mit_user_fullname from zerver.lib.zephyr import compute_mit_user_fullname
from zerver.models import Message, UserProfile, Stream, Subscription, Client,\ from zerver.models import Message, UserProfile, Stream, Subscription, Client,\
Realm, RealmDomain, Recipient, UserMessage, bulk_get_recipients, get_personal_recipient, \ Realm, RealmDomain, Recipient, UserMessage, bulk_get_recipients, get_personal_recipient, \
@ -300,13 +300,26 @@ class NarrowBuilder:
cond = self.msg_id_column == literal(operand) cond = self.msg_id_column == literal(operand)
return query.where(maybe_negate(cond)) return query.where(maybe_negate(cond))
def by_pm_with(self, query: Query, operand: str, maybe_negate: ConditionTransform) -> Query: def by_pm_with(self, query: Query, operand: Union[str, Iterable[int]],
maybe_negate: ConditionTransform) -> Query:
# This will strip our own email out of the operand # This will strip our own email out of the operand
# and do other munging. # and do other munging.
emails = raw_pm_with_emails( if isinstance(operand, str):
email_str=operand, emails = raw_pm_with_emails(
my_email=self.user_profile.email, email_str=operand,
) my_email=self.user_profile.email,
)
else:
"""
This is where we handle passing a list of user IDs for the narrow, which is the
preferred/cleaner API. Ideally, we'd refactor the below to natively use user IDs,
rather than having mapping user IDs to emails here.
"""
emails = raw_pm_with_emails_by_ids(
user_ids=operand,
my_email=self.user_profile.email,
realm=self.user_realm
)
if len(emails) == 0: if len(emails) == 0:
raise BadNarrowOperator('empty pm-with clause') raise BadNarrowOperator('empty pm-with clause')
@ -317,7 +330,7 @@ class NarrowBuilder:
recipient = recipient_for_emails(emails, False, recipient = recipient_for_emails(emails, False,
self.user_profile, self.user_profile) self.user_profile, self.user_profile)
except ValidationError: except ValidationError:
raise BadNarrowOperator('unknown recipient ' + operand) raise BadNarrowOperator('unknown recipient in ' + str(operand))
cond = column("recipient_id") == recipient.id cond = column("recipient_id") == recipient.id
return query.where(maybe_negate(cond)) return query.where(maybe_negate(cond))
else: else:
@ -336,7 +349,7 @@ class NarrowBuilder:
try: try:
narrow_profile = get_user_including_cross_realm(target_email, self.user_realm) narrow_profile = get_user_including_cross_realm(target_email, self.user_realm)
except UserProfile.DoesNotExist: except UserProfile.DoesNotExist:
raise BadNarrowOperator('unknown user ' + operand) raise BadNarrowOperator('unknown user in ' + str(operand))
narrow_recipient = get_personal_recipient(narrow_profile.id) narrow_recipient = get_personal_recipient(narrow_profile.id)
cond = or_(and_(column("sender_id") == narrow_profile.id, cond = or_(and_(column("sender_id") == narrow_profile.id,
@ -350,7 +363,7 @@ class NarrowBuilder:
try: try:
narrow_profile = get_user_including_cross_realm(operand, self.user_realm) narrow_profile = get_user_including_cross_realm(operand, self.user_realm)
except UserProfile.DoesNotExist: except UserProfile.DoesNotExist:
raise BadNarrowOperator('unknown user ' + operand) raise BadNarrowOperator('unknown user ' + str(operand))
self_recipient_ids = [ self_recipient_ids = [
recipient_tuple['recipient_id'] for recipient_tuple recipient_tuple['recipient_id'] for recipient_tuple
@ -494,9 +507,15 @@ def narrow_parameter(json: str) -> Optional[List[Dict[str, Any]]]:
return dict(operator=elem[0], operand=elem[1]) return dict(operator=elem[0], operand=elem[1])
if isinstance(elem, dict): if isinstance(elem, dict):
user_ids_supported_operators = ['pm-with']
if elem.get('operator', '') in user_ids_supported_operators:
operand_validator = check_string_or_int_list
else:
operand_validator = check_string
validator = check_dict([ validator = check_dict([
('operator', check_string), ('operator', check_string),
('operand', check_string), ('operand', operand_validator),
]) ])
error = validator('elem', elem) error = validator('elem', elem)