streams: Fix sending stream-related events to guests.

Previous behavior-
- Guest did not receive stream creation events for new
web-public streams.
- Guest did not receive peer_add and peer_remove events
for web-public and subscribed public streams.

This commit fixes the behavior to be -
- Guests now receive stream creation events for new
web-public streams.
- Guest now receive peer_add and peer_remove events for
web-public and subscribed public streams.
This commit is contained in:
Sahil Batra 2023-10-26 21:44:01 +05:30 committed by Tim Abbott
parent 71b8f49614
commit 9a6cf82adc
11 changed files with 161 additions and 63 deletions

View File

@ -20,6 +20,17 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 8.0 ## Changes in Zulip 8.0
**Feature level 220**
* [`GET /events`](/api/get-events): Stream creation events for web-public
streams are now sent to all guest users in the organization as well.
* [`GET /events`](/api/get-events): The `subscription` events for `op:
"peer_add"` and `op: "peer_remove"` are now sent to subscribed guest
users for public streams and to all the guest users for web-public
streams; previously, they incorrectly only received these for
private streams.
**Feature level 219** **Feature level 219**
* [`PATCH /realm/user_settings_defaults`](/api/update-realm-user-settings-defaults) * [`PATCH /realm/user_settings_defaults`](/api/update-realm-user-settings-defaults)

View File

@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
# Changes should be accompanied by documentation explaining what the # Changes should be accompanied by documentation explaining what the
# new level means in api_docs/changelog.md, as well as "**Changes**" # new level means in api_docs/changelog.md, as well as "**Changes**"
# entries in the endpoint's documentation in `zulip.yaml`. # entries in the endpoint's documentation in `zulip.yaml`.
API_FEATURE_LEVEL = 219 API_FEATURE_LEVEL = 220
# Bump the minor PROVISION_VERSION to indicate that folks should provision # Bump the minor PROVISION_VERSION to indicate that folks should provision
# only when going from an old version of the code to a newer version. Bump # only when going from an old version of the code to a newer version. Bump

View File

@ -674,5 +674,5 @@ def do_reactivate_user(user_profile: UserProfile, *, acting_user: Optional[UserP
realm=user_profile.realm, realm=user_profile.realm,
altered_user_dict=altered_user_dict, altered_user_dict=altered_user_dict,
stream_dict=stream_dict, stream_dict=stream_dict,
private_peer_dict=subscriber_peer_info.private_peer_dict, subscriber_peer_info=subscriber_peer_info,
) )

View File

@ -31,7 +31,7 @@ from zerver.lib.queue import queue_event_on_commit, queue_json_publish
from zerver.lib.stream_color import pick_colors from zerver.lib.stream_color import pick_colors
from zerver.lib.stream_subscription import ( from zerver.lib.stream_subscription import (
SubInfo, SubInfo,
bulk_get_private_peers, SubscriberPeerInfo,
bulk_get_subscriber_peer_info, bulk_get_subscriber_peer_info,
get_active_subscriptions_for_stream_id, get_active_subscriptions_for_stream_id,
get_bulk_stream_subscriber_info, get_bulk_stream_subscriber_info,
@ -62,6 +62,7 @@ from zerver.models import (
UserGroup, UserGroup,
UserProfile, UserProfile,
active_non_guest_user_ids, active_non_guest_user_ids,
active_user_ids,
get_system_bot, get_system_bot,
) )
from zerver.tornado.django_api import send_event, send_event_on_commit from zerver.tornado.django_api import send_event, send_event_on_commit
@ -466,7 +467,7 @@ def send_stream_creation_events_for_previously_inaccessible_streams(
# Realm admins already have all created private streams. # Realm admins already have all created private streams.
realm_admin_ids = {user.id for user in realm.get_admin_users_and_bots()} realm_admin_ids = {user.id for user in realm.get_admin_users_and_bots()}
notify_user_ids = list(stream_users_ids - realm_admin_ids) notify_user_ids = list(stream_users_ids - realm_admin_ids)
else: elif not stream.is_web_public:
# Guese users need a `create` notification for # Guese users need a `create` notification for
# public streams as well because they need the stream # public streams as well because they need the stream
# to exist before they get the "subscribe" notification. # to exist before they get the "subscribe" notification.
@ -481,7 +482,7 @@ def send_peer_subscriber_events(
realm: Realm, realm: Realm,
stream_dict: Dict[int, Stream], stream_dict: Dict[int, Stream],
altered_user_dict: Dict[int, Set[int]], altered_user_dict: Dict[int, Set[int]],
private_peer_dict: Dict[int, Set[int]], subscriber_peer_info: SubscriberPeerInfo,
) -> None: ) -> None:
# Send peer_add/peer_remove events to other users who are tracking the # Send peer_add/peer_remove events to other users who are tracking the
# subscribers lists of streams in their browser; everyone for # subscribers lists of streams in their browser; everyone for
@ -492,6 +493,7 @@ def send_peer_subscriber_events(
private_stream_ids = [ private_stream_ids = [
stream_id for stream_id in altered_user_dict if stream_dict[stream_id].invite_only stream_id for stream_id in altered_user_dict if stream_dict[stream_id].invite_only
] ]
private_peer_dict = subscriber_peer_info.private_peer_dict
for stream_id in private_stream_ids: for stream_id in private_stream_ids:
altered_user_ids = altered_user_dict[stream_id] altered_user_ids = altered_user_dict[stream_id]
@ -512,14 +514,27 @@ def send_peer_subscriber_events(
if not stream_dict[stream_id].invite_only and not stream_dict[stream_id].is_in_zephyr_realm if not stream_dict[stream_id].invite_only and not stream_dict[stream_id].is_in_zephyr_realm
] ]
web_public_stream_ids = [
stream_id for stream_id in public_stream_ids if stream_dict[stream_id].is_web_public
]
subscriber_dict = subscriber_peer_info.subscribed_ids
if public_stream_ids: if public_stream_ids:
user_streams: Dict[int, Set[int]] = defaultdict(set) user_streams: Dict[int, Set[int]] = defaultdict(set)
public_peer_ids = set(active_non_guest_user_ids(realm.id)) non_guest_user_ids = set(active_non_guest_user_ids(realm.id))
if web_public_stream_ids:
web_public_peer_ids = set(active_user_ids(realm.id))
for stream_id in public_stream_ids: for stream_id in public_stream_ids:
altered_user_ids = altered_user_dict[stream_id] altered_user_ids = altered_user_dict[stream_id]
peer_user_ids = public_peer_ids - altered_user_ids
if stream_id in web_public_stream_ids:
peer_user_ids = web_public_peer_ids - altered_user_ids
else:
peer_user_ids = (non_guest_user_ids | subscriber_dict[stream_id]) - altered_user_ids
if peer_user_ids and altered_user_ids: if peer_user_ids and altered_user_ids:
if len(altered_user_ids) == 1: if len(altered_user_ids) == 1:
@ -545,7 +560,11 @@ def send_peer_subscriber_events(
send_event_on_commit(realm, event, peer_user_ids) send_event_on_commit(realm, event, peer_user_ids)
for user_id, stream_ids in user_streams.items(): for user_id, stream_ids in user_streams.items():
peer_user_ids = public_peer_ids - {user_id} if stream_id in web_public_stream_ids:
peer_user_ids = web_public_peer_ids - altered_user_ids
else:
peer_user_ids = (non_guest_user_ids | subscriber_dict[stream_id]) - altered_user_ids
event = dict( event = dict(
type="subscription", type="subscription",
op=op, op=op,
@ -689,7 +708,7 @@ def bulk_add_subscriptions(
realm=realm, realm=realm,
altered_user_dict=altered_user_dict, altered_user_dict=altered_user_dict,
stream_dict=stream_dict, stream_dict=stream_dict,
private_peer_dict=subscriber_peer_info.private_peer_dict, subscriber_peer_info=subscriber_peer_info,
) )
return ( return (
@ -703,11 +722,9 @@ def send_peer_remove_events(
streams: List[Stream], streams: List[Stream],
altered_user_dict: Dict[int, Set[int]], altered_user_dict: Dict[int, Set[int]],
) -> None: ) -> None:
private_streams = [stream for stream in streams if stream.invite_only] subscriber_peer_info = bulk_get_subscriber_peer_info(
private_peer_dict = bulk_get_private_peers(
realm=realm, realm=realm,
private_streams=private_streams, streams=streams,
) )
stream_dict = {stream.id: stream for stream in streams} stream_dict = {stream.id: stream for stream in streams}
@ -716,7 +733,7 @@ def send_peer_remove_events(
realm=realm, realm=realm,
stream_dict=stream_dict, stream_dict=stream_dict,
altered_user_dict=altered_user_dict, altered_user_dict=altered_user_dict,
private_peer_dict=private_peer_dict, subscriber_peer_info=subscriber_peer_info,
) )

View File

@ -198,8 +198,7 @@ def bulk_get_subscriber_peer_info(
realm_admin_ids = {user.id for user in realm.get_admin_users_and_bots()} realm_admin_ids = {user.id for user in realm.get_admin_users_and_bots()}
for stream_id in private_stream_ids: for stream_id in private_stream_ids:
# This is the same business rule as we use in # Realm admins can see all private stream
# bulk_get_private_peers. Realm admins can see all private stream
# subscribers. # subscribers.
subscribed_user_ids = stream_user_ids.get(stream_id, set()) subscribed_user_ids = stream_user_ids.get(stream_id, set())
subscribed_ids[stream_id] = subscribed_user_ids subscribed_ids[stream_id] = subscribed_user_ids
@ -215,34 +214,6 @@ def bulk_get_subscriber_peer_info(
) )
def bulk_get_private_peers(
realm: Realm,
private_streams: List[Stream],
) -> Dict[int, Set[int]]:
if not private_streams:
return {}
for stream in private_streams:
# Our caller should only pass us private streams.
assert stream.invite_only
peer_ids: Dict[int, Set[int]] = {}
realm_admin_ids = {user.id for user in realm.get_admin_users_and_bots()}
stream_ids = {stream.id for stream in private_streams}
stream_user_ids = get_user_ids_for_streams(stream_ids)
for stream in private_streams:
# This is the same business rule as we use in
# bulk_get_subscriber_peer_info. Realm admins can see all private
# stream subscribers.
subscribed_user_ids = stream_user_ids.get(stream.id, set())
peer_ids[stream.id] = subscribed_user_ids | realm_admin_ids
return peer_ids
def handle_stream_notifications_compatibility( def handle_stream_notifications_compatibility(
user_profile: Optional[UserProfile], user_profile: Optional[UserProfile],
stream_dict: Dict[str, Any], stream_dict: Dict[str, Any],

View File

@ -31,6 +31,7 @@ from zerver.models import (
UserGroup, UserGroup,
UserProfile, UserProfile,
active_non_guest_user_ids, active_non_guest_user_ids,
active_user_ids,
bulk_get_streams, bulk_get_streams,
get_realm_stream, get_realm_stream,
get_stream, get_stream,
@ -179,7 +180,11 @@ def create_stream_if_needed(
) )
if created: if created:
if stream.is_public(): if stream.is_public():
send_stream_creation_event(realm, stream, active_non_guest_user_ids(stream.realm_id)) if stream.is_web_public:
notify_user_ids = active_user_ids(stream.realm_id)
else:
notify_user_ids = active_non_guest_user_ids(stream.realm_id)
send_stream_creation_event(realm, stream, notify_user_ids)
else: else:
realm_admin_ids = [user.id for user in stream.realm.get_admin_users_and_bots()] realm_admin_ids = [user.id for user in stream.realm.get_admin_users_and_bots()]
send_stream_creation_event(realm, stream, realm_admin_ids) send_stream_creation_event(realm, stream, realm_admin_ids)

View File

@ -1389,14 +1389,18 @@ Output:
# Subscribe to a stream directly # Subscribe to a stream directly
def subscribe( def subscribe(
self, user_profile: UserProfile, stream_name: str, invite_only: bool = False self,
user_profile: UserProfile,
stream_name: str,
invite_only: bool = False,
is_web_public: bool = False,
) -> Stream: ) -> Stream:
realm = user_profile.realm realm = user_profile.realm
try: try:
stream = get_stream(stream_name, user_profile.realm) stream = get_stream(stream_name, user_profile.realm)
except Stream.DoesNotExist: except Stream.DoesNotExist:
stream, from_stream_creation = create_stream_if_needed( stream, from_stream_creation = create_stream_if_needed(
realm, stream_name, invite_only=invite_only realm, stream_name, invite_only=invite_only, is_web_public=is_web_public
) )
bulk_add_subscriptions(realm, [stream], [user_profile], acting_user=None) bulk_add_subscriptions(realm, [stream], [user_profile], acting_user=None)
return stream return stream

View File

@ -782,8 +782,12 @@ paths:
to see a given stream's subscriber list, they will receive this event to see a given stream's subscriber list, they will receive this event
for the existing subscriptions to the stream. for the existing subscriptions to the stream.
**Changes**: Prior to Zulip 8.0 (feature level 205), this event was not **Changes**: Prior to Zulip 8.0 (feature level 220), this event was
sent when a user gained access to a stream due to their [role incorrectly not sent to guest users when subscribers to web-public
streams and subscribed public streams changed.
Prior to Zulip 8.0 (feature level 205), this event was not sent when
a user gained access to a stream due to their [role
changing](/help/roles-and-permissions). changing](/help/roles-and-permissions).
Prior to Zulip 6.0 (feature level 134), this event was not sent when a Prior to Zulip 6.0 (feature level 134), this event was not sent when a
@ -841,10 +845,13 @@ paths:
from streams. Sent to all users if the stream is public or to only from streams. Sent to all users if the stream is public or to only
the existing subscribers if the stream is private. the existing subscribers if the stream is private.
**Changes**: In Zulip 4.0 (feature level 35), the singular **Changes**: Prior to Zulip 8.0 (feature level 220), this event was
`user_id` and `stream_id` integers included in this event incorrectly not sent to guest users when subscribers to web-public
were replaced with plural `user_ids` and `stream_ids` integer streams and subscribed public streams changed.
arrays.
In Zulip 4.0 (feature level 35), the singular `user_id` and
`stream_id` integers included in this event were replaced
with plural `user_ids` and `stream_ids` integer arrays.
In Zulip 3.0 (feature level 19), the `stream_id` field was In Zulip 3.0 (feature level 19), the `stream_id` field was
added to identify the stream the user unsubscribed from, added to identify the stream the user unsubscribed from,
@ -1197,9 +1204,11 @@ paths:
Note that organization administrators who are not subscribed will Note that organization administrators who are not subscribed will
not be able to see content on the stream; just that it exists. not be able to see content on the stream; just that it exists.
**Changes**: Prior to Zulip 8.0 (feature level 205), this event was **Changes**: Prior to Zulip 8.0 (feature level 220), this event was
not sent when a user gained access to a stream due to their role incorrectly not sent to guest users a web-public stream was created.
changing.
Prior to Zulip 8.0 (feature level 205), this event was not sent
when a user gained access to a stream due to their role changing.
Prior to Zulip 8.0 (feature level 192), this event was not sent Prior to Zulip 8.0 (feature level 192), this event was not sent
when guest users gained access to a public stream by being when guest users gained access to a public stream by being

View File

@ -3557,6 +3557,14 @@ class UserDisplayActionTest(BaseAction):
action = lambda: self.subscribe(self.example_user("hamlet"), "Test stream 2") action = lambda: self.subscribe(self.example_user("hamlet"), "Test stream 2")
events = self.verify_action(action, num_events=0, state_change_expected=False) events = self.verify_action(action, num_events=0, state_change_expected=False)
# Check that guest user receives stream creation event for web-public stream.
action = lambda: self.subscribe(
self.example_user("hamlet"), "Web public test stream", is_web_public=True
)
events = self.verify_action(action, num_events=2, state_change_expected=True)
check_stream_create("events[0]", events[0])
check_subscription_peer_add("events[1]", events[1])
self.user_profile = self.example_user("hamlet") self.user_profile = self.example_user("hamlet")
action = lambda: self.subscribe( action = lambda: self.subscribe(
self.example_user("hamlet"), "Private test stream", invite_only=True self.example_user("hamlet"), "Private test stream", invite_only=True
@ -3779,8 +3787,32 @@ class SubscribeActionTest(BaseAction):
stream.invite_only = False stream.invite_only = False
stream.save() stream.save()
# Subscribe as a guest to a public stream. # Test events for guest user.
self.user_profile = self.example_user("polonius") self.user_profile = self.example_user("polonius")
# Guest user does not receive peer_add/peer_remove events for unsubscribed
# public streams.
action = lambda: bulk_add_subscriptions(
user_profile.realm, [stream], [self.example_user("othello")], acting_user=None
)
events = self.verify_action(
action,
include_subscribers=include_subscribers,
num_events=0,
state_change_expected=False,
)
action = lambda: bulk_remove_subscriptions(
user_profile.realm, [self.example_user("othello")], [stream], acting_user=None
)
events = self.verify_action(
action,
include_subscribers=include_subscribers,
num_events=0,
state_change_expected=False,
)
# Subscribe as a guest to a public stream.
action = lambda: bulk_add_subscriptions( action = lambda: bulk_add_subscriptions(
user_profile.realm, [stream], [self.user_profile], acting_user=None user_profile.realm, [stream], [self.user_profile], acting_user=None
) )
@ -3788,6 +3820,55 @@ class SubscribeActionTest(BaseAction):
check_stream_create("events[0]", events[0]) check_stream_create("events[0]", events[0])
check_subscription_add("events[1]", events[1]) check_subscription_add("events[1]", events[1])
action = lambda: bulk_add_subscriptions(
user_profile.realm, [stream], [self.example_user("othello")], acting_user=None
)
events = self.verify_action(
action,
include_subscribers=include_subscribers,
state_change_expected=include_subscribers,
)
check_subscription_peer_add("events[0]", events[0])
action = lambda: bulk_remove_subscriptions(
user_profile.realm, [self.example_user("othello")], [stream], acting_user=None
)
events = self.verify_action(
action,
include_subscribers=include_subscribers,
state_change_expected=include_subscribers,
)
check_subscription_peer_remove("events[0]", events[0])
stream = self.make_stream("web-public-stream", self.user_profile.realm, is_web_public=True)
# Guest user receives peer_add/peer_remove events for unsubscribed
# web-public streams.
action = lambda: bulk_add_subscriptions(
user_profile.realm, [stream], [self.example_user("othello")], acting_user=None
)
events = self.verify_action(
action,
include_subscribers=include_subscribers,
state_change_expected=include_subscribers,
)
action = lambda: bulk_remove_subscriptions(
user_profile.realm, [self.example_user("othello")], [stream], acting_user=None
)
events = self.verify_action(
action,
include_subscribers=include_subscribers,
state_change_expected=include_subscribers,
)
# Subscribe as a guest to web-public stream. Guest does not receive stream creation
# event for web-public stream.
action = lambda: bulk_add_subscriptions(
user_profile.realm, [stream], [self.user_profile], acting_user=None
)
events = self.verify_action(action, include_subscribers=include_subscribers, num_events=1)
check_subscription_add("events[0]", events[0])
class DraftActionTest(BaseAction): class DraftActionTest(BaseAction):
def do_enable_drafts_synchronization(self, user_profile: UserProfile) -> None: def do_enable_drafts_synchronization(self, user_profile: UserProfile) -> None:

View File

@ -142,7 +142,7 @@ class StreamSetupTest(ZulipTestCase):
new_user = self.create_simple_new_user(realm, new_user_email) new_user = self.create_simple_new_user(realm, new_user_email)
with self.assert_database_query_count(12): with self.assert_database_query_count(13):
set_up_streams_for_new_human_user( set_up_streams_for_new_human_user(
user_profile=new_user, user_profile=new_user,
prereg_user=prereg_user, prereg_user=prereg_user,

View File

@ -2598,7 +2598,7 @@ class StreamAdminTest(ZulipTestCase):
those you aren't on. those you aren't on.
""" """
result = self.attempt_unsubscribe_of_principal( result = self.attempt_unsubscribe_of_principal(
query_count=16, query_count=17,
target_users=[self.example_user("cordelia")], target_users=[self.example_user("cordelia")],
is_realm_admin=True, is_realm_admin=True,
is_subbed=True, is_subbed=True,
@ -2625,7 +2625,7 @@ class StreamAdminTest(ZulipTestCase):
for name in ["cordelia", "prospero", "iago", "hamlet", "outgoing_webhook_bot"] for name in ["cordelia", "prospero", "iago", "hamlet", "outgoing_webhook_bot"]
] ]
result = self.attempt_unsubscribe_of_principal( result = self.attempt_unsubscribe_of_principal(
query_count=27, query_count=28,
cache_count=8, cache_count=8,
target_users=target_users, target_users=target_users,
is_realm_admin=True, is_realm_admin=True,
@ -2686,7 +2686,7 @@ class StreamAdminTest(ZulipTestCase):
def test_admin_remove_others_from_stream_legacy_emails(self) -> None: def test_admin_remove_others_from_stream_legacy_emails(self) -> None:
result = self.attempt_unsubscribe_of_principal( result = self.attempt_unsubscribe_of_principal(
query_count=16, query_count=17,
target_users=[self.example_user("cordelia")], target_users=[self.example_user("cordelia")],
is_realm_admin=True, is_realm_admin=True,
is_subbed=True, is_subbed=True,
@ -2700,7 +2700,7 @@ class StreamAdminTest(ZulipTestCase):
def test_admin_remove_multiple_users_from_stream_legacy_emails(self) -> None: def test_admin_remove_multiple_users_from_stream_legacy_emails(self) -> None:
result = self.attempt_unsubscribe_of_principal( result = self.attempt_unsubscribe_of_principal(
query_count=19, query_count=20,
target_users=[self.example_user("cordelia"), self.example_user("prospero")], target_users=[self.example_user("cordelia"), self.example_user("prospero")],
is_realm_admin=True, is_realm_admin=True,
is_subbed=True, is_subbed=True,
@ -2747,7 +2747,7 @@ class StreamAdminTest(ZulipTestCase):
webhook_bot = self.example_user("webhook_bot") webhook_bot = self.example_user("webhook_bot")
do_change_bot_owner(webhook_bot, bot_owner=user_profile, acting_user=user_profile) do_change_bot_owner(webhook_bot, bot_owner=user_profile, acting_user=user_profile)
result = self.attempt_unsubscribe_of_principal( result = self.attempt_unsubscribe_of_principal(
query_count=13, query_count=14,
target_users=[webhook_bot], target_users=[webhook_bot],
is_realm_admin=False, is_realm_admin=False,
is_subbed=True, is_subbed=True,