mirror of https://github.com/zulip/zulip.git
message_fetch: Allow access to web-public msgs for unauth users.
Via API, users can now access messages which are in web-public streams without any authentication. If the user is not authenticated, we assume it is a web-public query and add `streams:web-public` narrow if not already present to the narrow. web-public streams are also directly accessible. Any malformed narrow which is not allowed in a web-public query results in a 400 or 401. See test_message_fetch for the allowed queries.
This commit is contained in:
parent
28b43b4edc
commit
9f9daeea5b
|
@ -23,7 +23,7 @@ def check_supported_events_narrow_filter(narrow: Iterable[Sequence[str]]) -> Non
|
||||||
if operator not in ["stream", "topic", "sender", "is"]:
|
if operator not in ["stream", "topic", "sender", "is"]:
|
||||||
raise JsonableError(_("Operator {} not supported.").format(operator))
|
raise JsonableError(_("Operator {} not supported.").format(operator))
|
||||||
|
|
||||||
def is_web_public_compatible(narrow: Iterable[Dict[str, str]]) -> bool:
|
def is_web_public_compatible(narrow: Iterable[Dict[str, Any]]) -> bool:
|
||||||
for element in narrow:
|
for element in narrow:
|
||||||
operator = element['operator']
|
operator = element['operator']
|
||||||
if 'operand' not in element:
|
if 'operand' not in element:
|
||||||
|
@ -32,6 +32,18 @@ def is_web_public_compatible(narrow: Iterable[Dict[str, str]]) -> bool:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def is_web_public_narrow(narrow: Optional[Iterable[Dict[str, Any]]]) -> bool:
|
||||||
|
if narrow is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for term in narrow:
|
||||||
|
# Web public queries are only allowed for limited types of narrows.
|
||||||
|
# term == {'operator': 'streams', 'operand': 'web-public', 'negated': False}
|
||||||
|
if term['operator'] == 'streams' and term['operand'] == 'web-public' and term['negated'] is False:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def build_narrow_filter(narrow: Iterable[Sequence[str]]) -> Callable[[Mapping[str, Any]], bool]:
|
def build_narrow_filter(narrow: Iterable[Sequence[str]]) -> Callable[[Mapping[str, Any]], bool]:
|
||||||
"""Changes to this function should come with corresponding changes to
|
"""Changes to this function should come with corresponding changes to
|
||||||
BuildNarrowFilterTest."""
|
BuildNarrowFilterTest."""
|
||||||
|
|
|
@ -285,6 +285,13 @@ def get_public_streams_queryset(realm: Realm) -> 'QuerySet[Stream]':
|
||||||
return Stream.objects.filter(realm=realm, invite_only=False,
|
return Stream.objects.filter(realm=realm, invite_only=False,
|
||||||
history_public_to_subscribers=True)
|
history_public_to_subscribers=True)
|
||||||
|
|
||||||
|
def get_web_public_streams_queryset(realm: Realm) -> 'QuerySet[Stream]':
|
||||||
|
# In theory, is_web_public=True implies invite_only=False and
|
||||||
|
# history_public_to_subscribers=True, but it's safer to include
|
||||||
|
# this in the query.
|
||||||
|
return Stream.objects.filter(realm=realm, deactivated=False, invite_only=False,
|
||||||
|
history_public_to_subscribers=True, is_web_public=True)
|
||||||
|
|
||||||
def get_stream_by_id(stream_id: int) -> Stream:
|
def get_stream_by_id(stream_id: int) -> Stream:
|
||||||
error = _("Invalid stream id")
|
error = _("Invalid stream id")
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -5,6 +5,7 @@ from unittest import mock
|
||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
|
from django.http import HttpResponse
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.utils.timezone import now as timezone_now
|
from django.utils.timezone import now as timezone_now
|
||||||
from sqlalchemy.sql import and_, column, select, table
|
from sqlalchemy.sql import and_, column, select, table
|
||||||
|
@ -18,6 +19,7 @@ from zerver.lib.actions import (
|
||||||
do_set_realm_property,
|
do_set_realm_property,
|
||||||
do_update_message,
|
do_update_message,
|
||||||
)
|
)
|
||||||
|
from zerver.lib.avatar import avatar_url
|
||||||
from zerver.lib.markdown import MentionData
|
from zerver.lib.markdown import MentionData
|
||||||
from zerver.lib.message import (
|
from zerver.lib.message import (
|
||||||
MessageDict,
|
MessageDict,
|
||||||
|
@ -93,7 +95,7 @@ class NarrowBuilderTest(ZulipTestCase):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.realm = get_realm('zulip')
|
self.realm = get_realm('zulip')
|
||||||
self.user_profile = self.example_user('hamlet')
|
self.user_profile = self.example_user('hamlet')
|
||||||
self.builder = NarrowBuilder(self.user_profile, column('id'))
|
self.builder = NarrowBuilder(self.user_profile, column('id'), self.realm)
|
||||||
self.raw_query = select([column("id")], None, table("zerver_message"))
|
self.raw_query = select([column("id")], None, table("zerver_message"))
|
||||||
self.hamlet_email = self.example_user('hamlet').email
|
self.hamlet_email = self.example_user('hamlet').email
|
||||||
self.othello_email = self.example_user('othello').email
|
self.othello_email = self.example_user('othello').email
|
||||||
|
@ -447,6 +449,16 @@ class NarrowBuilderTest(ZulipTestCase):
|
||||||
query = self._build_query(term)
|
query = self._build_query(term)
|
||||||
self.assertEqual(get_sqlalchemy_sql(query), 'SELECT id \nFROM zerver_message')
|
self.assertEqual(get_sqlalchemy_sql(query), 'SELECT id \nFROM zerver_message')
|
||||||
|
|
||||||
|
def test_add_term_non_web_public_stream_in_web_public_query(self) -> None:
|
||||||
|
self.make_stream('non-web-public-stream', realm=self.realm)
|
||||||
|
term = dict(operator='stream', operand='non-web-public-stream')
|
||||||
|
builder = NarrowBuilder(self.user_profile, column('id'), self.realm, True)
|
||||||
|
|
||||||
|
def _build_query(term: Dict[str, Any]) -> Query:
|
||||||
|
return builder.add_term(self.raw_query, term)
|
||||||
|
|
||||||
|
self.assertRaises(BadNarrowOperator, _build_query, term)
|
||||||
|
|
||||||
def _do_add_term_test(self, term: Dict[str, Any], where_clause: str,
|
def _do_add_term_test(self, term: Dict[str, Any], where_clause: str,
|
||||||
params: Optional[Dict[str, Any]]=None) -> None:
|
params: Optional[Dict[str, Any]]=None) -> None:
|
||||||
query = self._build_query(term)
|
query = self._build_query(term)
|
||||||
|
@ -523,19 +535,19 @@ class IncludeHistoryTest(ZulipTestCase):
|
||||||
narrow = [
|
narrow = [
|
||||||
dict(operator='stream', operand='public_stream', negated=True),
|
dict(operator='stream', operand='public_stream', negated=True),
|
||||||
]
|
]
|
||||||
self.assertFalse(ok_to_include_history(narrow, user_profile))
|
self.assertFalse(ok_to_include_history(narrow, user_profile, False))
|
||||||
|
|
||||||
# streams:public searches should include history for non-guest members.
|
# streams:public searches should include history for non-guest members.
|
||||||
narrow = [
|
narrow = [
|
||||||
dict(operator='streams', operand='public'),
|
dict(operator='streams', operand='public'),
|
||||||
]
|
]
|
||||||
self.assertTrue(ok_to_include_history(narrow, user_profile))
|
self.assertTrue(ok_to_include_history(narrow, user_profile, False))
|
||||||
|
|
||||||
# Negated -streams:public searches should not include history.
|
# Negated -streams:public searches should not include history.
|
||||||
narrow = [
|
narrow = [
|
||||||
dict(operator='streams', operand='public', negated=True),
|
dict(operator='streams', operand='public', negated=True),
|
||||||
]
|
]
|
||||||
self.assertFalse(ok_to_include_history(narrow, user_profile))
|
self.assertFalse(ok_to_include_history(narrow, user_profile, False))
|
||||||
|
|
||||||
# Definitely forbid seeing history on private streams.
|
# Definitely forbid seeing history on private streams.
|
||||||
self.make_stream('private_stream', realm=user_profile.realm, invite_only=True)
|
self.make_stream('private_stream', realm=user_profile.realm, invite_only=True)
|
||||||
|
@ -544,7 +556,7 @@ class IncludeHistoryTest(ZulipTestCase):
|
||||||
narrow = [
|
narrow = [
|
||||||
dict(operator='stream', operand='private_stream'),
|
dict(operator='stream', operand='private_stream'),
|
||||||
]
|
]
|
||||||
self.assertFalse(ok_to_include_history(narrow, user_profile))
|
self.assertFalse(ok_to_include_history(narrow, user_profile, False))
|
||||||
|
|
||||||
# Verify that with stream.history_public_to_subscribers, subscribed
|
# Verify that with stream.history_public_to_subscribers, subscribed
|
||||||
# users can access history.
|
# users can access history.
|
||||||
|
@ -555,20 +567,20 @@ class IncludeHistoryTest(ZulipTestCase):
|
||||||
narrow = [
|
narrow = [
|
||||||
dict(operator='stream', operand='private_stream_2'),
|
dict(operator='stream', operand='private_stream_2'),
|
||||||
]
|
]
|
||||||
self.assertFalse(ok_to_include_history(narrow, user_profile))
|
self.assertFalse(ok_to_include_history(narrow, user_profile, False))
|
||||||
self.assertTrue(ok_to_include_history(narrow, subscribed_user_profile))
|
self.assertTrue(ok_to_include_history(narrow, subscribed_user_profile, False))
|
||||||
|
|
||||||
# History doesn't apply to PMs.
|
# History doesn't apply to PMs.
|
||||||
narrow = [
|
narrow = [
|
||||||
dict(operator='is', operand='private'),
|
dict(operator='is', operand='private'),
|
||||||
]
|
]
|
||||||
self.assertFalse(ok_to_include_history(narrow, user_profile))
|
self.assertFalse(ok_to_include_history(narrow, user_profile, False))
|
||||||
|
|
||||||
# History doesn't apply to unread messages.
|
# History doesn't apply to unread messages.
|
||||||
narrow = [
|
narrow = [
|
||||||
dict(operator='is', operand='unread'),
|
dict(operator='is', operand='unread'),
|
||||||
]
|
]
|
||||||
self.assertFalse(ok_to_include_history(narrow, user_profile))
|
self.assertFalse(ok_to_include_history(narrow, user_profile, False))
|
||||||
|
|
||||||
# If we are looking for something like starred messages, there is
|
# If we are looking for something like starred messages, there is
|
||||||
# no point in searching historical messages.
|
# no point in searching historical messages.
|
||||||
|
@ -576,7 +588,7 @@ class IncludeHistoryTest(ZulipTestCase):
|
||||||
dict(operator='stream', operand='public_stream'),
|
dict(operator='stream', operand='public_stream'),
|
||||||
dict(operator='is', operand='starred'),
|
dict(operator='is', operand='starred'),
|
||||||
]
|
]
|
||||||
self.assertFalse(ok_to_include_history(narrow, user_profile))
|
self.assertFalse(ok_to_include_history(narrow, user_profile, False))
|
||||||
|
|
||||||
# No point in searching history for is operator even if included with
|
# No point in searching history for is operator even if included with
|
||||||
# streams:public
|
# streams:public
|
||||||
|
@ -584,30 +596,30 @@ class IncludeHistoryTest(ZulipTestCase):
|
||||||
dict(operator='streams', operand='public'),
|
dict(operator='streams', operand='public'),
|
||||||
dict(operator='is', operand='mentioned'),
|
dict(operator='is', operand='mentioned'),
|
||||||
]
|
]
|
||||||
self.assertFalse(ok_to_include_history(narrow, user_profile))
|
self.assertFalse(ok_to_include_history(narrow, user_profile, False))
|
||||||
narrow = [
|
narrow = [
|
||||||
dict(operator='streams', operand='public'),
|
dict(operator='streams', operand='public'),
|
||||||
dict(operator='is', operand='unread'),
|
dict(operator='is', operand='unread'),
|
||||||
]
|
]
|
||||||
self.assertFalse(ok_to_include_history(narrow, user_profile))
|
self.assertFalse(ok_to_include_history(narrow, user_profile, False))
|
||||||
narrow = [
|
narrow = [
|
||||||
dict(operator='streams', operand='public'),
|
dict(operator='streams', operand='public'),
|
||||||
dict(operator='is', operand='alerted'),
|
dict(operator='is', operand='alerted'),
|
||||||
]
|
]
|
||||||
self.assertFalse(ok_to_include_history(narrow, user_profile))
|
self.assertFalse(ok_to_include_history(narrow, user_profile, False))
|
||||||
|
|
||||||
# simple True case
|
# simple True case
|
||||||
narrow = [
|
narrow = [
|
||||||
dict(operator='stream', operand='public_stream'),
|
dict(operator='stream', operand='public_stream'),
|
||||||
]
|
]
|
||||||
self.assertTrue(ok_to_include_history(narrow, user_profile))
|
self.assertTrue(ok_to_include_history(narrow, user_profile, False))
|
||||||
|
|
||||||
narrow = [
|
narrow = [
|
||||||
dict(operator='stream', operand='public_stream'),
|
dict(operator='stream', operand='public_stream'),
|
||||||
dict(operator='topic', operand='whatever'),
|
dict(operator='topic', operand='whatever'),
|
||||||
dict(operator='search', operand='needle in haystack'),
|
dict(operator='search', operand='needle in haystack'),
|
||||||
]
|
]
|
||||||
self.assertTrue(ok_to_include_history(narrow, user_profile))
|
self.assertTrue(ok_to_include_history(narrow, user_profile, False))
|
||||||
|
|
||||||
# Tests for guest user
|
# Tests for guest user
|
||||||
guest_user_profile = self.example_user("polonius")
|
guest_user_profile = self.example_user("polonius")
|
||||||
|
@ -618,23 +630,23 @@ class IncludeHistoryTest(ZulipTestCase):
|
||||||
narrow = [
|
narrow = [
|
||||||
dict(operator='streams', operand='public'),
|
dict(operator='streams', operand='public'),
|
||||||
]
|
]
|
||||||
self.assertFalse(ok_to_include_history(narrow, guest_user_profile))
|
self.assertFalse(ok_to_include_history(narrow, guest_user_profile, False))
|
||||||
|
|
||||||
# Guest user can't access public stream
|
# Guest user can't access public stream
|
||||||
self.subscribe(subscribed_user_profile, 'public_stream_2')
|
self.subscribe(subscribed_user_profile, 'public_stream_2')
|
||||||
narrow = [
|
narrow = [
|
||||||
dict(operator='stream', operand='public_stream_2'),
|
dict(operator='stream', operand='public_stream_2'),
|
||||||
]
|
]
|
||||||
self.assertFalse(ok_to_include_history(narrow, guest_user_profile))
|
self.assertFalse(ok_to_include_history(narrow, guest_user_profile, False))
|
||||||
self.assertTrue(ok_to_include_history(narrow, subscribed_user_profile))
|
self.assertTrue(ok_to_include_history(narrow, subscribed_user_profile, False))
|
||||||
|
|
||||||
# Definitely, a guest user can't access the unsubscribed private stream
|
# Definitely, a guest user can't access the unsubscribed private stream
|
||||||
self.subscribe(subscribed_user_profile, 'private_stream_3')
|
self.subscribe(subscribed_user_profile, 'private_stream_3')
|
||||||
narrow = [
|
narrow = [
|
||||||
dict(operator='stream', operand='private_stream_3'),
|
dict(operator='stream', operand='private_stream_3'),
|
||||||
]
|
]
|
||||||
self.assertFalse(ok_to_include_history(narrow, guest_user_profile))
|
self.assertFalse(ok_to_include_history(narrow, guest_user_profile, False))
|
||||||
self.assertTrue(ok_to_include_history(narrow, subscribed_user_profile))
|
self.assertTrue(ok_to_include_history(narrow, subscribed_user_profile, False))
|
||||||
|
|
||||||
# Guest user can access (history of) subscribed private streams
|
# Guest user can access (history of) subscribed private streams
|
||||||
self.subscribe(guest_user_profile, 'private_stream_4')
|
self.subscribe(guest_user_profile, 'private_stream_4')
|
||||||
|
@ -642,8 +654,8 @@ class IncludeHistoryTest(ZulipTestCase):
|
||||||
narrow = [
|
narrow = [
|
||||||
dict(operator='stream', operand='private_stream_4'),
|
dict(operator='stream', operand='private_stream_4'),
|
||||||
]
|
]
|
||||||
self.assertTrue(ok_to_include_history(narrow, guest_user_profile))
|
self.assertTrue(ok_to_include_history(narrow, guest_user_profile, False))
|
||||||
self.assertTrue(ok_to_include_history(narrow, subscribed_user_profile))
|
self.assertTrue(ok_to_include_history(narrow, subscribed_user_profile, False))
|
||||||
|
|
||||||
class PostProcessTest(ZulipTestCase):
|
class PostProcessTest(ZulipTestCase):
|
||||||
def test_basics(self) -> None:
|
def test_basics(self) -> None:
|
||||||
|
@ -1192,6 +1204,157 @@ class GetOldMessagesTest(ZulipTestCase):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_unauthenticated_get_messages_non_existant_realm(self) -> None:
|
||||||
|
post_params = {
|
||||||
|
"anchor": 10000000000000000,
|
||||||
|
"num_before": 5,
|
||||||
|
"num_after": 1,
|
||||||
|
"narrow": orjson.dumps([dict(operator='streams', operand="web-public")]).decode(),
|
||||||
|
}
|
||||||
|
|
||||||
|
with mock.patch('zerver.views.message_fetch.get_realm_from_request', return_value=None):
|
||||||
|
result = self.client_get("/json/messages", dict(post_params))
|
||||||
|
self.assert_json_error(result, "Invalid subdomain.",
|
||||||
|
status_code=400)
|
||||||
|
|
||||||
|
def test_unauthenticated_get_messages_without_web_public(self) -> None:
|
||||||
|
"""
|
||||||
|
An unauthenticated call to GET /json/messages with valid parameters
|
||||||
|
returns a 401.
|
||||||
|
"""
|
||||||
|
post_params = {
|
||||||
|
"anchor": 1,
|
||||||
|
"num_before": 1,
|
||||||
|
"num_after": 1,
|
||||||
|
"narrow": orjson.dumps([dict(operator='is', operand="private")]).decode(),
|
||||||
|
}
|
||||||
|
result = self.client_get("/json/messages", dict(post_params))
|
||||||
|
self.assert_json_error(result, "Not logged in: API authentication or user session required",
|
||||||
|
status_code=401)
|
||||||
|
|
||||||
|
post_params = {
|
||||||
|
"anchor": 10000000000000000,
|
||||||
|
"num_before": 5,
|
||||||
|
"num_after": 1,
|
||||||
|
}
|
||||||
|
result = self.client_get("/json/messages", dict(post_params))
|
||||||
|
self.assert_json_error(result, "Not logged in: API authentication or user session required",
|
||||||
|
status_code=401)
|
||||||
|
|
||||||
|
def test_unauthenticated_get_messages_with_web_public(self) -> None:
|
||||||
|
"""
|
||||||
|
An unauthenticated call to GET /json/messages without valid
|
||||||
|
parameters in the `streams:web-public` narrow returns a 401.
|
||||||
|
"""
|
||||||
|
post_params: Dict[str, Union[int, str, bool]] = {
|
||||||
|
"anchor": 1,
|
||||||
|
"num_before": 1,
|
||||||
|
"num_after": 1,
|
||||||
|
# "is:private" is not a is_web_public_compatible narrow.
|
||||||
|
"narrow": orjson.dumps([dict(operator="streams", operand="web-public"),
|
||||||
|
dict(operator="is", operand="private")]).decode(),
|
||||||
|
}
|
||||||
|
result = self.client_get("/json/messages", dict(post_params))
|
||||||
|
self.assert_json_error(result, 'Not logged in: API authentication or user session required',
|
||||||
|
status_code=401)
|
||||||
|
|
||||||
|
def test_unauthenticated_narrow_to_non_web_public_streams_without_web_public(self) -> None:
|
||||||
|
"""
|
||||||
|
An unauthenticated call to GET /json/messages without `streams:web-public` narrow returns a 401.
|
||||||
|
"""
|
||||||
|
post_params: Dict[str, Union[int, str, bool]] = {
|
||||||
|
"anchor": 1,
|
||||||
|
"num_before": 1,
|
||||||
|
"num_after": 1,
|
||||||
|
"narrow": orjson.dumps([dict(operator='stream', operand='Scotland')]).decode(),
|
||||||
|
}
|
||||||
|
result = self.client_get("/json/messages", dict(post_params))
|
||||||
|
self.assert_json_error(result, "Not logged in: API authentication or user session required",
|
||||||
|
status_code=401)
|
||||||
|
|
||||||
|
def test_unauthenticated_narrow_to_non_web_public_streams_with_web_public(self) -> None:
|
||||||
|
"""
|
||||||
|
An unauthenticated call to GET /json/messages with valid
|
||||||
|
parameters in the `streams:web-public` narrow + narrow to stream returns
|
||||||
|
a 400 if the target stream is not web-public.
|
||||||
|
"""
|
||||||
|
post_params: Dict[str, Union[int, str, bool]] = {
|
||||||
|
"anchor": 1,
|
||||||
|
"num_before": 1,
|
||||||
|
"num_after": 1,
|
||||||
|
"narrow": orjson.dumps([dict(operator="streams", operand="web-public"),
|
||||||
|
dict(operator='stream', operand='Scotland')]).decode(),
|
||||||
|
}
|
||||||
|
result = self.client_get("/json/messages", dict(post_params))
|
||||||
|
self.assert_json_error(result, 'Invalid narrow operator: unknown web-public stream Scotland',
|
||||||
|
status_code=400)
|
||||||
|
|
||||||
|
def setup_web_public_test(self, num_web_public_message: int=1) -> None:
|
||||||
|
"""
|
||||||
|
Send N+2 messages, N in a web-public stream, then one in a non web-public stream
|
||||||
|
and then a private message.
|
||||||
|
"""
|
||||||
|
user_profile = self.example_user('iago')
|
||||||
|
self.login('iago')
|
||||||
|
web_public_stream = self.make_stream('web-public-stream', is_web_public=True)
|
||||||
|
non_web_public_stream = self.make_stream('non-web-public-stream')
|
||||||
|
self.subscribe(user_profile, web_public_stream.name)
|
||||||
|
self.subscribe(user_profile, non_web_public_stream.name)
|
||||||
|
|
||||||
|
for _ in range(num_web_public_message):
|
||||||
|
self.send_stream_message(user_profile, web_public_stream.name,
|
||||||
|
content="web-public message")
|
||||||
|
self.send_stream_message(user_profile, non_web_public_stream.name,
|
||||||
|
content="non web-public message")
|
||||||
|
self.send_personal_message(user_profile, self.example_user('hamlet'),
|
||||||
|
content="private message")
|
||||||
|
self.logout()
|
||||||
|
|
||||||
|
def verify_web_public_query_result_success(self, result: HttpResponse, expected_num_messages: int) -> None:
|
||||||
|
self.assert_json_success(result)
|
||||||
|
messages = orjson.loads(result.content)['messages']
|
||||||
|
self.assert_length(messages, expected_num_messages)
|
||||||
|
sender = self.example_user('iago')
|
||||||
|
for msg in messages:
|
||||||
|
self.assertEqual(msg['content'], '<p>web-public message</p>')
|
||||||
|
self.assertEqual(msg['flags'], ['read'])
|
||||||
|
self.assertEqual(msg['sender_email'], sender.email)
|
||||||
|
self.assertEqual(msg['avatar_url'], avatar_url(sender))
|
||||||
|
|
||||||
|
def test_unauthenticated_narrow_to_web_public_streams(self) -> None:
|
||||||
|
self.setup_web_public_test()
|
||||||
|
|
||||||
|
post_params: Dict[str, Union[int, str, bool]] = {
|
||||||
|
"anchor": 1,
|
||||||
|
"num_before": 1,
|
||||||
|
"num_after": 1,
|
||||||
|
"narrow": orjson.dumps([dict(operator="streams", operand="web-public"),
|
||||||
|
dict(operator='stream', operand='web-public-stream')]).decode(),
|
||||||
|
}
|
||||||
|
result = self.client_get("/json/messages", dict(post_params))
|
||||||
|
self.verify_web_public_query_result_success(result, 1)
|
||||||
|
|
||||||
|
def test_get_messages_with_web_public(self) -> None:
|
||||||
|
"""
|
||||||
|
An unauthenticated call to GET /json/messages with valid parameters
|
||||||
|
including `streams:web-public` narrow returns list of messages in the
|
||||||
|
`web-public` streams.
|
||||||
|
"""
|
||||||
|
self.setup_web_public_test(num_web_public_message=8)
|
||||||
|
|
||||||
|
post_params = {
|
||||||
|
"anchor": "first_unread",
|
||||||
|
"num_before": 5,
|
||||||
|
"num_after": 1,
|
||||||
|
"narrow": orjson.dumps([dict(operator='streams', operand="web-public")]).decode(),
|
||||||
|
}
|
||||||
|
result = self.client_get("/json/messages", dict(post_params))
|
||||||
|
# Of the last 7 (num_before + num_after + 1) messages, only 5
|
||||||
|
# messages are returned, which were all web-public messages.
|
||||||
|
# The other two messages should not be returned even though
|
||||||
|
# they are the most recent.
|
||||||
|
self.verify_web_public_query_result_success(result, 5)
|
||||||
|
|
||||||
def test_client_avatar(self) -> None:
|
def test_client_avatar(self) -> None:
|
||||||
"""
|
"""
|
||||||
The client_gravatar flag determines whether we send avatar_url.
|
The client_gravatar flag determines whether we send avatar_url.
|
||||||
|
|
|
@ -37,10 +37,11 @@ class PublicURLTest(ZulipTestCase):
|
||||||
"/en/accounts/login/", "/ru/accounts/login/",
|
"/en/accounts/login/", "/ru/accounts/login/",
|
||||||
"/help/"],
|
"/help/"],
|
||||||
302: ["/", "/en/", "/ru/"],
|
302: ["/", "/en/", "/ru/"],
|
||||||
|
400: ["/json/messages",
|
||||||
|
],
|
||||||
401: [f"/json/streams/{denmark_stream_id}/members",
|
401: [f"/json/streams/{denmark_stream_id}/members",
|
||||||
"/api/v1/users/me/subscriptions",
|
"/api/v1/users/me/subscriptions",
|
||||||
"/api/v1/messages",
|
"/api/v1/messages",
|
||||||
"/json/messages",
|
|
||||||
"/api/v1/streams",
|
"/api/v1/streams",
|
||||||
],
|
],
|
||||||
404: ["/help/nonexistent", "/help/include/admin",
|
404: ["/help/nonexistent", "/help/include/admin",
|
||||||
|
|
|
@ -3,6 +3,7 @@ from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
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
|
||||||
|
@ -26,18 +27,21 @@ from sqlalchemy.sql import (
|
||||||
union_all,
|
union_all,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from zerver.context_processors import get_realm_from_request
|
||||||
from zerver.decorator import REQ, has_request_variables
|
from zerver.decorator import REQ, has_request_variables
|
||||||
from zerver.lib.actions import recipient_for_user_profiles
|
from zerver.lib.actions import 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
|
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.narrow import is_web_public_compatible, is_web_public_narrow
|
||||||
|
from zerver.lib.response import json_error, json_success, json_unauthorized
|
||||||
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 (
|
||||||
can_access_stream_history_by_id,
|
can_access_stream_history_by_id,
|
||||||
can_access_stream_history_by_name,
|
can_access_stream_history_by_name,
|
||||||
get_public_streams_queryset,
|
get_public_streams_queryset,
|
||||||
get_stream_by_narrow_operand_access_unchecked,
|
get_stream_by_narrow_operand_access_unchecked,
|
||||||
|
get_web_public_streams_queryset,
|
||||||
)
|
)
|
||||||
from zerver.lib.topic import DB_TOPIC_NAME, MATCH_TOPIC, topic_column_sa, topic_match_sa
|
from zerver.lib.topic import DB_TOPIC_NAME, MATCH_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
|
||||||
|
@ -132,10 +136,12 @@ class NarrowBuilder:
|
||||||
# * anything that would pull in additional rows, or information on
|
# * anything that would pull in additional rows, or information on
|
||||||
# other messages.
|
# other messages.
|
||||||
|
|
||||||
def __init__(self, user_profile: UserProfile, msg_id_column: str) -> None:
|
def __init__(self, user_profile: Optional[UserProfile], msg_id_column: str,
|
||||||
|
realm: Realm, is_web_public_query: bool=False) -> None:
|
||||||
self.user_profile = user_profile
|
self.user_profile = user_profile
|
||||||
self.msg_id_column = msg_id_column
|
self.msg_id_column = msg_id_column
|
||||||
self.realm = user_profile.realm
|
self.realm = realm
|
||||||
|
self.is_web_public_query = is_web_public_query
|
||||||
|
|
||||||
def add_term(self, query: Query, term: Dict[str, Any]) -> Query:
|
def add_term(self, query: Query, term: Dict[str, Any]) -> Query:
|
||||||
"""
|
"""
|
||||||
|
@ -178,6 +184,10 @@ class NarrowBuilder:
|
||||||
return query.where(maybe_negate(cond))
|
return query.where(maybe_negate(cond))
|
||||||
|
|
||||||
def by_in(self, query: Query, operand: str, maybe_negate: ConditionTransform) -> Query:
|
def by_in(self, query: Query, operand: str, maybe_negate: ConditionTransform) -> Query:
|
||||||
|
# This operator does not support is_web_public_query.
|
||||||
|
assert not self.is_web_public_query
|
||||||
|
assert self.user_profile is not None
|
||||||
|
|
||||||
if operand == 'home':
|
if operand == 'home':
|
||||||
conditions = exclude_muting_conditions(self.user_profile, [])
|
conditions = exclude_muting_conditions(self.user_profile, [])
|
||||||
return query.where(and_(*conditions))
|
return query.where(and_(*conditions))
|
||||||
|
@ -187,6 +197,10 @@ class NarrowBuilder:
|
||||||
raise BadNarrowOperator("unknown 'in' operand " + operand)
|
raise BadNarrowOperator("unknown 'in' operand " + operand)
|
||||||
|
|
||||||
def by_is(self, query: Query, operand: str, maybe_negate: ConditionTransform) -> Query:
|
def by_is(self, query: Query, operand: str, maybe_negate: ConditionTransform) -> Query:
|
||||||
|
# This operator class does not support is_web_public_query.
|
||||||
|
assert not self.is_web_public_query
|
||||||
|
assert self.user_profile is not None
|
||||||
|
|
||||||
if operand == 'private':
|
if operand == 'private':
|
||||||
cond = column("flags").op("&")(UserMessage.flags.is_private.mask) != 0
|
cond = column("flags").op("&")(UserMessage.flags.is_private.mask) != 0
|
||||||
return query.where(maybe_negate(cond))
|
return query.where(maybe_negate(cond))
|
||||||
|
@ -234,6 +248,9 @@ class NarrowBuilder:
|
||||||
# private streams you are no longer subscribed to, we
|
# private streams you are no longer subscribed to, we
|
||||||
# need get_stream_by_narrow_operand_access_unchecked here.
|
# need get_stream_by_narrow_operand_access_unchecked here.
|
||||||
stream = get_stream_by_narrow_operand_access_unchecked(operand, self.realm)
|
stream = get_stream_by_narrow_operand_access_unchecked(operand, self.realm)
|
||||||
|
|
||||||
|
if self.is_web_public_query and not stream.is_web_public:
|
||||||
|
raise BadNarrowOperator('unknown web-public stream ' + str(operand))
|
||||||
except Stream.DoesNotExist:
|
except Stream.DoesNotExist:
|
||||||
raise BadNarrowOperator('unknown stream ' + str(operand))
|
raise BadNarrowOperator('unknown stream ' + str(operand))
|
||||||
|
|
||||||
|
@ -269,6 +286,8 @@ class NarrowBuilder:
|
||||||
# Get all both subscribed and non subscribed public streams
|
# Get all both subscribed and non subscribed public streams
|
||||||
# but exclude any private subscribed streams.
|
# but exclude any private subscribed streams.
|
||||||
recipient_queryset = get_public_streams_queryset(self.realm)
|
recipient_queryset = get_public_streams_queryset(self.realm)
|
||||||
|
elif operand == 'web-public':
|
||||||
|
recipient_queryset = get_web_public_streams_queryset(self.realm)
|
||||||
else:
|
else:
|
||||||
raise BadNarrowOperator('unknown streams operand ' + operand)
|
raise BadNarrowOperator('unknown streams operand ' + operand)
|
||||||
|
|
||||||
|
@ -344,6 +363,9 @@ class NarrowBuilder:
|
||||||
|
|
||||||
def by_pm_with(self, query: Query, operand: Union[str, Iterable[int]],
|
def by_pm_with(self, query: Query, operand: Union[str, Iterable[int]],
|
||||||
maybe_negate: ConditionTransform) -> Query:
|
maybe_negate: ConditionTransform) -> Query:
|
||||||
|
# This operator does not support is_web_public_query.
|
||||||
|
assert not self.is_web_public_query
|
||||||
|
assert self.user_profile is not None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if isinstance(operand, str):
|
if isinstance(operand, str):
|
||||||
|
@ -405,6 +427,10 @@ class NarrowBuilder:
|
||||||
|
|
||||||
def by_group_pm_with(self, query: Query, operand: Union[str, int],
|
def by_group_pm_with(self, query: Query, operand: Union[str, int],
|
||||||
maybe_negate: ConditionTransform) -> Query:
|
maybe_negate: ConditionTransform) -> Query:
|
||||||
|
# This operator does not support is_web_public_query.
|
||||||
|
assert not self.is_web_public_query
|
||||||
|
assert self.user_profile is not None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if isinstance(operand, str):
|
if isinstance(operand, str):
|
||||||
narrow_profile = get_user_including_cross_realm(operand, self.realm)
|
narrow_profile = get_user_including_cross_realm(operand, self.realm)
|
||||||
|
@ -580,7 +606,8 @@ def narrow_parameter(json: str) -> OptionalNarrowListT:
|
||||||
|
|
||||||
return list(map(convert_term, data))
|
return list(map(convert_term, data))
|
||||||
|
|
||||||
def ok_to_include_history(narrow: OptionalNarrowListT, user_profile: UserProfile) -> bool:
|
def ok_to_include_history(narrow: OptionalNarrowListT, user_profile: Optional[UserProfile],
|
||||||
|
is_web_public_query: bool) -> bool:
|
||||||
# There are occasions where we need to find Message rows that
|
# There are occasions where we need to find Message rows that
|
||||||
# have no corresponding UserMessage row, because the user is
|
# have no corresponding UserMessage row, because the user is
|
||||||
# reading a public stream that might include messages that
|
# reading a public stream that might include messages that
|
||||||
|
@ -591,6 +618,17 @@ def ok_to_include_history(narrow: OptionalNarrowListT, user_profile: UserProfile
|
||||||
# query that narrows to a particular public stream on the user's realm.
|
# query that narrows to a particular public stream on the user's realm.
|
||||||
# If we screw this up, then we can get into a nasty situation of
|
# If we screw this up, then we can get into a nasty situation of
|
||||||
# polluting our narrow results with messages from other realms.
|
# polluting our narrow results with messages from other realms.
|
||||||
|
|
||||||
|
# For web-public queries, we are always returning history. The
|
||||||
|
# analogues of the below stream access checks for whether streams
|
||||||
|
# have is_web_public set and banning is operators in this code
|
||||||
|
# path are done directly in NarrowBuilder.
|
||||||
|
if is_web_public_query:
|
||||||
|
assert user_profile is None
|
||||||
|
return True
|
||||||
|
|
||||||
|
assert user_profile is not None
|
||||||
|
|
||||||
include_history = False
|
include_history = False
|
||||||
if narrow is not None:
|
if narrow is not None:
|
||||||
for term in narrow:
|
for term in narrow:
|
||||||
|
@ -650,7 +688,7 @@ def exclude_muting_conditions(user_profile: UserProfile,
|
||||||
|
|
||||||
return conditions
|
return conditions
|
||||||
|
|
||||||
def get_base_query_for_search(user_profile: UserProfile,
|
def get_base_query_for_search(user_profile: Optional[UserProfile],
|
||||||
need_message: bool,
|
need_message: bool,
|
||||||
need_user_message: bool) -> Tuple[Query, ColumnElement]:
|
need_user_message: bool) -> Tuple[Query, ColumnElement]:
|
||||||
# Handle the simple case where user_message isn't involved first.
|
# Handle the simple case where user_message isn't involved first.
|
||||||
|
@ -662,6 +700,7 @@ def get_base_query_for_search(user_profile: UserProfile,
|
||||||
inner_msg_id_col = literal_column("zerver_message.id")
|
inner_msg_id_col = literal_column("zerver_message.id")
|
||||||
return (query, inner_msg_id_col)
|
return (query, inner_msg_id_col)
|
||||||
|
|
||||||
|
assert user_profile is not None
|
||||||
if need_message:
|
if need_message:
|
||||||
query = select([column("message_id"), column("flags")],
|
query = select([column("message_id"), column("flags")],
|
||||||
column("user_profile_id") == literal(user_profile.id),
|
column("user_profile_id") == literal(user_profile.id),
|
||||||
|
@ -677,17 +716,19 @@ def get_base_query_for_search(user_profile: UserProfile,
|
||||||
inner_msg_id_col = column("message_id")
|
inner_msg_id_col = column("message_id")
|
||||||
return (query, inner_msg_id_col)
|
return (query, inner_msg_id_col)
|
||||||
|
|
||||||
def add_narrow_conditions(user_profile: UserProfile,
|
def add_narrow_conditions(user_profile: Optional[UserProfile],
|
||||||
inner_msg_id_col: ColumnElement,
|
inner_msg_id_col: ColumnElement,
|
||||||
query: Query,
|
query: Query,
|
||||||
narrow: OptionalNarrowListT) -> Tuple[Query, bool]:
|
narrow: OptionalNarrowListT,
|
||||||
|
is_web_public_query: bool,
|
||||||
|
realm: Realm) -> Tuple[Query, bool]:
|
||||||
is_search = False # for now
|
is_search = False # for now
|
||||||
|
|
||||||
if narrow is None:
|
if narrow is None:
|
||||||
return (query, is_search)
|
return (query, is_search)
|
||||||
|
|
||||||
# Build the query for the narrow
|
# Build the query for the narrow
|
||||||
builder = NarrowBuilder(user_profile, inner_msg_id_col)
|
builder = NarrowBuilder(user_profile, inner_msg_id_col, realm, is_web_public_query)
|
||||||
search_operands = []
|
search_operands = []
|
||||||
|
|
||||||
# As we loop through terms, builder does most of the work to extend
|
# As we loop through terms, builder does most of the work to extend
|
||||||
|
@ -711,8 +752,13 @@ def add_narrow_conditions(user_profile: UserProfile,
|
||||||
return (query, is_search)
|
return (query, is_search)
|
||||||
|
|
||||||
def find_first_unread_anchor(sa_conn: Any,
|
def find_first_unread_anchor(sa_conn: Any,
|
||||||
user_profile: UserProfile,
|
user_profile: Optional[UserProfile],
|
||||||
narrow: OptionalNarrowListT) -> int:
|
narrow: OptionalNarrowListT) -> int:
|
||||||
|
# For anonymous web users, all messages are treated as read, and so
|
||||||
|
# always return LARGER_THAN_MAX_MESSAGE_ID.
|
||||||
|
if user_profile is None:
|
||||||
|
return LARGER_THAN_MAX_MESSAGE_ID
|
||||||
|
|
||||||
# We always need UserMessage in our query, because it has the unread
|
# We always need UserMessage in our query, because it has the unread
|
||||||
# flag for the user.
|
# flag for the user.
|
||||||
need_user_message = True
|
need_user_message = True
|
||||||
|
@ -735,6 +781,8 @@ def find_first_unread_anchor(sa_conn: Any,
|
||||||
inner_msg_id_col=inner_msg_id_col,
|
inner_msg_id_col=inner_msg_id_col,
|
||||||
query=query,
|
query=query,
|
||||||
narrow=narrow,
|
narrow=narrow,
|
||||||
|
is_web_public_query=False,
|
||||||
|
realm=user_profile.realm,
|
||||||
)
|
)
|
||||||
|
|
||||||
condition = column("flags").op("&")(UserMessage.flags.read.mask) == 0
|
condition = column("flags").op("&")(UserMessage.flags.read.mask) == 0
|
||||||
|
@ -792,7 +840,8 @@ def parse_anchor_value(anchor_val: Optional[str],
|
||||||
raise JsonableError(_("Invalid anchor"))
|
raise JsonableError(_("Invalid anchor"))
|
||||||
|
|
||||||
@has_request_variables
|
@has_request_variables
|
||||||
def get_messages_backend(request: HttpRequest, user_profile: UserProfile,
|
def get_messages_backend(request: HttpRequest,
|
||||||
|
maybe_user_profile: Union[UserProfile, AnonymousUser],
|
||||||
anchor_val: Optional[str]=REQ(
|
anchor_val: Optional[str]=REQ(
|
||||||
'anchor', str_validator=check_string, default=None),
|
'anchor', str_validator=check_string, default=None),
|
||||||
num_before: int=REQ(converter=to_non_negative_int),
|
num_before: int=REQ(converter=to_non_negative_int),
|
||||||
|
@ -808,19 +857,54 @@ def get_messages_backend(request: HttpRequest, user_profile: UserProfile,
|
||||||
MAX_MESSAGES_PER_FETCH,
|
MAX_MESSAGES_PER_FETCH,
|
||||||
))
|
))
|
||||||
|
|
||||||
if user_profile.realm.email_address_visibility != Realm.EMAIL_ADDRESS_VISIBILITY_EVERYONE:
|
if not maybe_user_profile.is_authenticated:
|
||||||
|
# If user is not authenticated, clients must include
|
||||||
|
# `streams:web-public` in their narrow query to indicate this
|
||||||
|
# is a web-public query. This helps differentiate between
|
||||||
|
# cases of web-public queries (where we should return the
|
||||||
|
# web-public results only) and clients with buggy
|
||||||
|
# authentication code (where we should return an auth error).
|
||||||
|
if not is_web_public_narrow(narrow):
|
||||||
|
return json_unauthorized()
|
||||||
|
assert narrow is not None
|
||||||
|
if not is_web_public_compatible(narrow):
|
||||||
|
return json_unauthorized()
|
||||||
|
|
||||||
|
realm = get_realm_from_request(request)
|
||||||
|
if realm is None:
|
||||||
|
return json_error(_("Invalid subdomain."))
|
||||||
|
|
||||||
|
# We use None to indicate unauthenticated requests as it's more
|
||||||
|
# readable than using AnonymousUser, and the lack of Django
|
||||||
|
# stubs means that mypy can't check AnonymousUser well.
|
||||||
|
user_profile: Optional[UserProfile] = None
|
||||||
|
is_web_public_query = True
|
||||||
|
else:
|
||||||
|
assert isinstance(maybe_user_profile, UserProfile)
|
||||||
|
user_profile = maybe_user_profile
|
||||||
|
assert user_profile is not None
|
||||||
|
realm = user_profile.realm
|
||||||
|
is_web_public_query = False
|
||||||
|
|
||||||
|
assert realm is not None
|
||||||
|
|
||||||
|
if is_web_public_query or \
|
||||||
|
realm.email_address_visibility != Realm.EMAIL_ADDRESS_VISIBILITY_EVERYONE:
|
||||||
# If email addresses are only available to administrators,
|
# If email addresses are only available to administrators,
|
||||||
# clients cannot compute gravatars, so we force-set it to false.
|
# clients cannot compute gravatars, so we force-set it to false.
|
||||||
client_gravatar = False
|
client_gravatar = False
|
||||||
|
|
||||||
include_history = ok_to_include_history(narrow, user_profile)
|
include_history = ok_to_include_history(narrow, user_profile, is_web_public_query)
|
||||||
if include_history:
|
if include_history:
|
||||||
# The initial query in this case doesn't use `zerver_usermessage`,
|
# The initial query in this case doesn't use `zerver_usermessage`,
|
||||||
# and isn't yet limited to messages the user is entitled to see!
|
# and isn't yet limited to messages the user is entitled to see!
|
||||||
#
|
#
|
||||||
# This is OK only because we've made sure this is a narrow that
|
# This is OK only because we've made sure this is a narrow that
|
||||||
# will cause us to limit the query appropriately later.
|
# will cause us to limit the query appropriately elsewhere.
|
||||||
# See `ok_to_include_history` for details.
|
# See `ok_to_include_history` for details.
|
||||||
|
#
|
||||||
|
# Note that is_web_public_query=True goes here, since
|
||||||
|
# include_history is semantically correct for is_web_public_query.
|
||||||
need_message = True
|
need_message = True
|
||||||
need_user_message = False
|
need_user_message = False
|
||||||
elif narrow is None:
|
elif narrow is None:
|
||||||
|
@ -843,6 +927,8 @@ def get_messages_backend(request: HttpRequest, user_profile: UserProfile,
|
||||||
inner_msg_id_col=inner_msg_id_col,
|
inner_msg_id_col=inner_msg_id_col,
|
||||||
query=query,
|
query=query,
|
||||||
narrow=narrow,
|
narrow=narrow,
|
||||||
|
realm=realm,
|
||||||
|
is_web_public_query=is_web_public_query,
|
||||||
)
|
)
|
||||||
|
|
||||||
if narrow is not None:
|
if narrow is not None:
|
||||||
|
@ -858,7 +944,7 @@ def get_messages_backend(request: HttpRequest, user_profile: UserProfile,
|
||||||
sa_conn = get_sqlalchemy_connection()
|
sa_conn = get_sqlalchemy_connection()
|
||||||
|
|
||||||
if anchor is None:
|
if anchor is None:
|
||||||
# The use_first_unread_anchor code path
|
# `anchor=None` corresponds to the anchor="first_unread" parameter.
|
||||||
anchor = find_first_unread_anchor(
|
anchor = find_first_unread_anchor(
|
||||||
sa_conn,
|
sa_conn,
|
||||||
user_profile,
|
user_profile,
|
||||||
|
@ -873,7 +959,8 @@ def get_messages_backend(request: HttpRequest, user_profile: UserProfile,
|
||||||
if anchored_to_right:
|
if anchored_to_right:
|
||||||
num_after = 0
|
num_after = 0
|
||||||
|
|
||||||
first_visible_message_id = get_first_visible_message_id(user_profile.realm)
|
first_visible_message_id = get_first_visible_message_id(realm)
|
||||||
|
|
||||||
query = limit_query_to_range(
|
query = limit_query_to_range(
|
||||||
query=query,
|
query=query,
|
||||||
num_before=num_before,
|
num_before=num_before,
|
||||||
|
@ -912,7 +999,14 @@ def get_messages_backend(request: HttpRequest, user_profile: UserProfile,
|
||||||
# 'messages' list.
|
# 'messages' list.
|
||||||
message_ids: List[int] = []
|
message_ids: List[int] = []
|
||||||
user_message_flags: Dict[int, List[str]] = {}
|
user_message_flags: Dict[int, List[str]] = {}
|
||||||
if include_history:
|
if is_web_public_query:
|
||||||
|
# For web-public users, we treat all historical messages as read.
|
||||||
|
for row in rows:
|
||||||
|
message_id = row[0]
|
||||||
|
message_ids.append(message_id)
|
||||||
|
user_message_flags[message_id] = ["read"]
|
||||||
|
elif include_history:
|
||||||
|
assert user_profile is not None
|
||||||
message_ids = [row[0] for row in rows]
|
message_ids = [row[0] for row in rows]
|
||||||
|
|
||||||
# TODO: This could be done with an outer join instead of two queries
|
# TODO: This could be done with an outer join instead of two queries
|
||||||
|
@ -951,7 +1045,7 @@ def get_messages_backend(request: HttpRequest, user_profile: UserProfile,
|
||||||
search_fields=search_fields,
|
search_fields=search_fields,
|
||||||
apply_markdown=apply_markdown,
|
apply_markdown=apply_markdown,
|
||||||
client_gravatar=client_gravatar,
|
client_gravatar=client_gravatar,
|
||||||
allow_edit_history=user_profile.realm.allow_edit_history,
|
allow_edit_history=realm.allow_edit_history,
|
||||||
)
|
)
|
||||||
|
|
||||||
statsd.incr('loaded_old_messages', len(message_list))
|
statsd.incr('loaded_old_messages', len(message_list))
|
||||||
|
@ -1111,6 +1205,7 @@ def post_process_limited_query(rows: List[Any],
|
||||||
found_oldest=found_oldest,
|
found_oldest=found_oldest,
|
||||||
history_limited=history_limited,
|
history_limited=history_limited,
|
||||||
)
|
)
|
||||||
|
|
||||||
@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)),
|
||||||
|
@ -1128,7 +1223,7 @@ def messages_in_narrow_backend(request: HttpRequest, user_profile: UserProfile,
|
||||||
literal_column("zerver_usermessage.message_id") ==
|
literal_column("zerver_usermessage.message_id") ==
|
||||||
literal_column("zerver_message.id")))
|
literal_column("zerver_message.id")))
|
||||||
|
|
||||||
builder = NarrowBuilder(user_profile, column("message_id"))
|
builder = NarrowBuilder(user_profile, column("message_id"), user_profile.realm)
|
||||||
if narrow is not None:
|
if narrow is not None:
|
||||||
for term in narrow:
|
for term in narrow:
|
||||||
query = builder.add_term(query, term)
|
query = builder.add_term(query, term)
|
||||||
|
|
|
@ -208,7 +208,8 @@ v1_api_and_json_patterns = [
|
||||||
# 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
|
||||||
path('messages', rest_dispatch,
|
path('messages', rest_dispatch,
|
||||||
{'GET': 'zerver.views.message_fetch.get_messages_backend',
|
{'GET': ('zerver.views.message_fetch.get_messages_backend',
|
||||||
|
{'allow_anonymous_user_web'}),
|
||||||
'POST': ('zerver.views.message_send.send_message_backend',
|
'POST': ('zerver.views.message_send.send_message_backend',
|
||||||
{'allow_incoming_webhooks'})}),
|
{'allow_incoming_webhooks'})}),
|
||||||
path('messages/<int:message_id>', rest_dispatch,
|
path('messages/<int:message_id>', rest_dispatch,
|
||||||
|
|
Loading…
Reference in New Issue