zulip/zerver/lib/home.py

252 lines
9.1 KiB
Python
Raw Normal View History

import calendar
import time
from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple
from django.conf import settings
from django.http import HttpRequest
from django.utils import translation
from two_factor.utils import default_device
from zerver.context_processors import get_apps_page_url
from zerver.lib.events import do_events_register
from zerver.lib.i18n import (
get_and_set_request_language,
get_language_list,
get_language_translation_data,
)
from zerver.lib.realm_description import get_realm_rendered_description
from zerver.lib.request import RequestNotes
from zerver.models import Message, Realm, Stream, UserProfile
from zerver.views.message_flags import get_latest_update_message_flag_activity
from zproject.config import get_config
@dataclass
class BillingInfo:
show_billing: bool
show_plans: bool
@dataclass
class UserPermissionInfo:
color_scheme: int
is_guest: bool
is_realm_admin: bool
is_realm_owner: bool
show_webathena: bool
def get_furthest_read_time(user_profile: Optional[UserProfile]) -> Optional[float]:
if user_profile is None:
return time.time()
user_activity = get_latest_update_message_flag_activity(user_profile)
if user_activity is None:
return None
return calendar.timegm(user_activity.last_visit.utctimetuple())
def get_bot_types(user_profile: Optional[UserProfile]) -> List[Dict[str, object]]:
bot_types: List[Dict[str, object]] = []
if user_profile is None:
return bot_types
for type_id, name in UserProfile.BOT_TYPES.items():
bot_types.append(
dict(
type_id=type_id,
name=name,
allowed=type_id in user_profile.allowed_bot_types,
)
)
return bot_types
def promote_sponsoring_zulip_in_realm(realm: Realm) -> bool:
if not settings.PROMOTE_SPONSORING_ZULIP:
return False
# If PROMOTE_SPONSORING_ZULIP is enabled, advertise sponsoring
# Zulip in the gear menu of non-paying organizations.
return realm.plan_type in [Realm.PLAN_TYPE_STANDARD_FREE, Realm.PLAN_TYPE_SELF_HOSTED]
def get_billing_info(user_profile: Optional[UserProfile]) -> BillingInfo:
show_billing = False
show_plans = False
if settings.CORPORATE_ENABLED and user_profile is not None:
if user_profile.has_billing_access:
from corporate.models import CustomerPlan, get_customer_by_realm
customer = get_customer_by_realm(user_profile.realm)
if customer is not None:
if customer.sponsorship_pending:
show_billing = True
elif CustomerPlan.objects.filter(customer=customer).exists():
show_billing = True
if not user_profile.is_guest and user_profile.realm.plan_type == Realm.PLAN_TYPE_LIMITED:
show_plans = True
return BillingInfo(
show_billing=show_billing,
show_plans=show_plans,
)
def get_user_permission_info(user_profile: Optional[UserProfile]) -> UserPermissionInfo:
if user_profile is not None:
return UserPermissionInfo(
color_scheme=user_profile.color_scheme,
is_guest=user_profile.is_guest,
is_realm_owner=user_profile.is_realm_owner,
is_realm_admin=user_profile.is_realm_admin,
show_webathena=user_profile.realm.webathena_enabled,
)
else:
return UserPermissionInfo(
color_scheme=UserProfile.COLOR_SCHEME_AUTOMATIC,
is_guest=False,
is_realm_admin=False,
is_realm_owner=False,
show_webathena=False,
)
def build_page_params_for_home_page_load(
request: HttpRequest,
user_profile: Optional[UserProfile],
realm: Realm,
insecure_desktop_app: bool,
narrow: List[List[str]],
narrow_stream: Optional[Stream],
narrow_topic: Optional[str],
first_in_realm: bool,
prompt_for_invites: bool,
needs_tutorial: bool,
) -> Tuple[int, Dict[str, object]]:
"""
This function computes page_params for when we load the home page.
The page_params data structure gets sent to the client.
"""
client_capabilities = {
"notification_settings_null": True,
"bulk_message_deletion": True,
"user_avatar_url_field_optional": True,
"stream_typing_notifications": False, # Set this to True when frontend support is implemented.
"user_settings_object": True,
linkifier: Support URL templates for linkifiers. This swaps out url_format_string from all of our APIs and replaces it with url_template. Note that the documentation changes in the following commits will be squashed with this commit. We change the "url_format" key to "url_template" for the realm_linkifiers events in event_schema, along with updating LinkifierDict. "url_template" is the name chosen to normalize mixed usages of "url_format_string" and "url_format" throughout the backend. The markdown processor is updated to stop handling the format string interpolation and delegate the task template expansion to the uri_template library instead. This change affects many test cases. We mostly just replace "%(name)s" with "{name}", "url_format_string" with "url_template" to make sure that they still pass. There are some test cases dedicated for testing "%" escaping, which aren't relevant anymore and are subject to removal. But for now we keep most of them as-is, and make sure that "%" is always escaped since we do not use it for variable substitution any more. Since url_format_string is not populated anymore, a migration is created to remove this field entirely, and make url_template non-nullable since we will always populate it. Note that it is possible to have url_template being null after migration 0422 and before 0424, but in practice, url_template will not be None after backfilling and the backend now is always setting url_template. With the removal of url_format_string, RealmFilter model will now be cleaned with URL template checks, and the old checks for escapes are removed. We also modified RealmFilter.clean to skip the validation when the url_template is invalid. This avoids raising mulitple ValidationError's when calling full_clean on a linkifier. But we might eventually want to have a more centric approach to data validation instead of having the same validation in both the clean method and the validator. Fixes #23124. Signed-off-by: Zixuan James Li <p359101898@gmail.com>
2022-10-05 20:55:31 +02:00
"linkifier_url_template": True,
}
if user_profile is not None:
client = RequestNotes.get_notes(request).client
assert client is not None
register_ret = do_events_register(
user_profile,
realm,
client,
apply_markdown=True,
client_gravatar=True,
slim_presence=True,
client_capabilities=client_capabilities,
narrow=narrow,
include_streams=False,
)
home: Don't send /register response for spectators. In very large communities, computing page_params can be quite expensive. Because we've moved the homepage for communities with web public streams enabled to be the Zulip app, and it's common for automation to frequently poll the homepage of a Zulip organization, we'd like to keep those homepages cheap (as the login pages are). We address this by prototyping something we may end up wanting to do anyway -- having the web application do a `POST /register` API call in order to fetch most page_params, and merging those with the mostly webapp configuration page_params that we leave in the / response for convenience. This exact implementation is messy in a few ways: * We rely on the assumption that ui_init.initialize_everything happens before all code that needs to inspect the page_params properties we are fetching via /register. This is likely mostly true, but nothing in the implementation enforces it. * The bundle of ~25 keys that are in page_params ideally would be considered individually, with some moved to the /register API response and perhaps others eliminated or namespaced inside a webapp_settings object. * It's weird to have the spectators network sequence different that from logged-in users, and potentially a maintainability risk. * We might be able to arrange that the initial `/` response be cacheable, now that we're no longer embedding our metadata inside it. We've made no effort to do that as of yet. Despite those issues, this commit solves an immediate problem and will give us helpful experience with a model closer to the one we'll want in order to happily support a web client that can be run locally against a production Zulip server's data. Co-authored-by: Anders Kaseorg <anders@zulip.com>
2022-05-03 23:22:41 +02:00
default_language = register_ret["user_settings"]["default_language"]
else:
home: Don't send /register response for spectators. In very large communities, computing page_params can be quite expensive. Because we've moved the homepage for communities with web public streams enabled to be the Zulip app, and it's common for automation to frequently poll the homepage of a Zulip organization, we'd like to keep those homepages cheap (as the login pages are). We address this by prototyping something we may end up wanting to do anyway -- having the web application do a `POST /register` API call in order to fetch most page_params, and merging those with the mostly webapp configuration page_params that we leave in the / response for convenience. This exact implementation is messy in a few ways: * We rely on the assumption that ui_init.initialize_everything happens before all code that needs to inspect the page_params properties we are fetching via /register. This is likely mostly true, but nothing in the implementation enforces it. * The bundle of ~25 keys that are in page_params ideally would be considered individually, with some moved to the /register API response and perhaps others eliminated or namespaced inside a webapp_settings object. * It's weird to have the spectators network sequence different that from logged-in users, and potentially a maintainability risk. * We might be able to arrange that the initial `/` response be cacheable, now that we're no longer embedding our metadata inside it. We've made no effort to do that as of yet. Despite those issues, this commit solves an immediate problem and will give us helpful experience with a model closer to the one we'll want in order to happily support a web client that can be run locally against a production Zulip server's data. Co-authored-by: Anders Kaseorg <anders@zulip.com>
2022-05-03 23:22:41 +02:00
# The spectator client will be fetching the /register response
# for spectators via the API. But we still need to set the
# values not presence in that object.
register_ret = {
"queue_id": None,
}
default_language = realm.default_language
if user_profile is None:
request_language = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME, default_language)
else:
request_language = get_and_set_request_language(
request,
default_language,
translation.get_language_from_path(request.path_info),
)
furthest_read_time = get_furthest_read_time(user_profile)
two_fa_enabled = settings.TWO_FACTOR_AUTHENTICATION_ENABLED and user_profile is not None
billing_info = get_billing_info(user_profile)
user_permission_info = get_user_permission_info(user_profile)
# Pass parameters to the client-side JavaScript code.
# These end up in a JavaScript Object named 'page_params'.
page_params: Dict[str, object] = dict(
## Server settings.
test_suite=settings.TEST_SUITE,
insecure_desktop_app=insecure_desktop_app,
login_page=settings.HOME_NOT_LOGGED_IN,
warn_no_email=settings.WARN_NO_EMAIL,
# Only show marketing email settings if on Zulip Cloud
corporate_enabled=settings.CORPORATE_ENABLED,
## Misc. extra data.
language_list=get_language_list(),
needs_tutorial=needs_tutorial,
first_in_realm=first_in_realm,
prompt_for_invites=prompt_for_invites,
furthest_read_time=furthest_read_time,
bot_types=get_bot_types(user_profile),
two_fa_enabled=two_fa_enabled,
apps_page_url=get_apps_page_url(),
show_billing=billing_info.show_billing,
promote_sponsoring_zulip=promote_sponsoring_zulip_in_realm(realm),
show_plans=billing_info.show_plans,
show_webathena=user_permission_info.show_webathena,
# Adding two_fa_enabled as condition saves us 3 queries when
# 2FA is not enabled.
two_fa_enabled_user=two_fa_enabled and bool(default_device(user_profile)),
is_spectator=user_profile is None,
# There is no event queue for spectators since
# events support for spectators is not implemented yet.
no_event_queue=user_profile is None,
server_sentry_dsn=settings.SENTRY_FRONTEND_DSN,
)
if settings.SENTRY_FRONTEND_DSN is not None:
page_params["realm_sentry_key"] = realm.string_id
page_params["server_sentry_environment"] = get_config(
"machine", "deploy_type", "development"
)
page_params["server_sentry_sample_rate"] = settings.SENTRY_FRONTEND_SAMPLE_RATE
page_params["server_sentry_trace_rate"] = settings.SENTRY_FRONTEND_TRACE_RATE
for field_name in register_ret:
page_params[field_name] = register_ret[field_name]
if narrow_stream is not None:
# In narrow_stream context, initial pointer is just latest message
recipient = narrow_stream.recipient
try:
max_message_id = (
Message.objects.filter(recipient=recipient).order_by("id").reverse()[0].id
)
except IndexError:
max_message_id = -1
page_params["narrow_stream"] = narrow_stream.name
if narrow_topic is not None:
page_params["narrow_topic"] = narrow_topic
page_params["narrow"] = [dict(operator=term[0], operand=term[1]) for term in narrow]
page_params["max_message_id"] = max_message_id
assert isinstance(page_params["user_settings"], dict)
page_params["user_settings"]["enable_desktop_notifications"] = False
page_params["translation_data"] = get_language_translation_data(request_language)
if user_profile is None:
# Get rendered version of realm description which is displayed in right
# sidebar for spectator.
page_params["realm_rendered_description"] = get_realm_rendered_description(realm)
page_params["language_cookie_name"] = settings.LANGUAGE_COOKIE_NAME
return register_ret["queue_id"], page_params