2020-07-18 20:33:28 +02:00
|
|
|
import calendar
|
|
|
|
import time
|
2020-07-18 18:13:59 +02:00
|
|
|
from dataclasses import dataclass
|
2022-06-21 22:53:25 +02:00
|
|
|
from typing import Dict, List, Optional, Tuple
|
2020-07-18 18:13:59 +02:00
|
|
|
|
|
|
|
from django.conf import settings
|
2020-07-18 20:33:28 +02:00
|
|
|
from django.http import HttpRequest
|
|
|
|
from django.utils import translation
|
|
|
|
from two_factor.utils import default_device
|
2020-07-18 18:13:59 +02:00
|
|
|
|
2021-06-14 12:38:43 +02:00
|
|
|
from zerver.context_processors import get_apps_page_url
|
2020-07-18 20:33:28 +02:00
|
|
|
from zerver.lib.events import do_events_register
|
|
|
|
from zerver.lib.i18n import (
|
2020-10-02 14:23:45 +02:00
|
|
|
get_and_set_request_language,
|
2020-07-18 20:33:28 +02:00
|
|
|
get_language_list,
|
|
|
|
get_language_translation_data,
|
|
|
|
)
|
2023-06-30 02:48:22 +02:00
|
|
|
from zerver.lib.narrow_helpers import NarrowTerm
|
2022-02-15 10:58:53 +01:00
|
|
|
from zerver.lib.realm_description import get_realm_rendered_description
|
2021-08-21 19:24:20 +02:00
|
|
|
from zerver.lib.request import RequestNotes
|
2020-07-18 20:33:28 +02:00
|
|
|
from zerver.models import Message, Realm, Stream, UserProfile
|
|
|
|
from zerver.views.message_flags import get_latest_update_message_flag_activity
|
sentry: Add frontend event monitoring.
Zulip already has integrations for server-side Sentry integration;
however, it has historically used the Zulip-specific `blueslip`
library for monitoring browser-side errors. However, the latter sends
errors to email, as well optionally to an internal `#errors` stream.
While this is sufficient for low volumes of users, and useful in that
it does not rely on outside services, at higher volumes it is very
difficult to do any analysis or filtering of the errors. Client-side
errors are exceptionally noisy, with many false positives due to
browser extensions or similar, so determining real real errors from a
stream of un-grouped emails or messages in a stream is quite
difficult.
Add a client-side Javascript sentry integration. To provide useful
backtraces, this requires extending the pre-deploy hooks to upload the
source-maps to Sentry. Additional keys are added to the non-public
API of `page_params` to control the DSN, realm identifier, and sample
rates.
2023-02-13 20:50:57 +01:00
|
|
|
from zproject.config import get_config
|
2020-07-18 18:13:59 +02:00
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class BillingInfo:
|
|
|
|
show_billing: bool
|
|
|
|
show_plans: bool
|
2023-11-04 14:24:04 +01:00
|
|
|
sponsorship_pending: bool
|
2023-11-15 22:44:24 +01:00
|
|
|
show_remote_billing: bool
|
2020-07-18 18:13:59 +02:00
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class UserPermissionInfo:
|
|
|
|
color_scheme: int
|
|
|
|
is_guest: bool
|
|
|
|
is_realm_admin: bool
|
|
|
|
is_realm_owner: bool
|
|
|
|
show_webathena: bool
|
|
|
|
|
|
|
|
|
2020-07-18 20:33:28 +02:00
|
|
|
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]] = []
|
2020-09-27 06:49:16 +02:00
|
|
|
if user_profile is None:
|
2020-07-18 20:33:28 +02:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2021-03-05 18:39:02 +01:00
|
|
|
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.
|
2021-10-18 23:28:17 +02:00
|
|
|
return realm.plan_type in [Realm.PLAN_TYPE_STANDARD_FREE, Realm.PLAN_TYPE_SELF_HOSTED]
|
2021-03-05 18:39:02 +01:00
|
|
|
|
|
|
|
|
2021-06-15 14:24:35 +02:00
|
|
|
def get_billing_info(user_profile: Optional[UserProfile]) -> BillingInfo:
|
2023-11-04 14:24:04 +01:00
|
|
|
# See https://zulip.com/help/roles-and-permissions for clarity.
|
2020-07-18 18:13:59 +02:00
|
|
|
show_billing = False
|
|
|
|
show_plans = False
|
2023-11-04 14:24:04 +01:00
|
|
|
sponsorship_pending = False
|
2024-02-28 20:03:34 +01:00
|
|
|
|
|
|
|
# We want to always show the remote billing option as long as the user is authorized,
|
|
|
|
# except on zulipchat.com where it's not applicable.
|
2023-11-15 22:44:24 +01:00
|
|
|
show_remote_billing = (
|
2024-02-28 20:03:34 +01:00
|
|
|
(not settings.CORPORATE_ENABLED)
|
|
|
|
and user_profile is not None
|
|
|
|
and user_profile.has_billing_access
|
2023-11-15 22:44:24 +01:00
|
|
|
)
|
|
|
|
|
2023-11-04 14:24:04 +01:00
|
|
|
# This query runs on home page load, so we want to avoid
|
|
|
|
# hitting the database if possible. So, we only run it for the user
|
|
|
|
# types that can actually see the billing info.
|
2023-11-21 17:36:11 +01:00
|
|
|
if settings.CORPORATE_ENABLED and user_profile is not None and user_profile.has_billing_access:
|
2023-11-04 14:24:04 +01:00
|
|
|
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:
|
|
|
|
sponsorship_pending = True
|
|
|
|
|
|
|
|
if CustomerPlan.objects.filter(customer=customer).exists():
|
|
|
|
show_billing = True
|
|
|
|
|
|
|
|
if user_profile.realm.plan_type == Realm.PLAN_TYPE_LIMITED:
|
2020-07-18 18:13:59 +02:00
|
|
|
show_plans = True
|
|
|
|
|
2021-03-05 18:39:02 +01:00
|
|
|
return BillingInfo(
|
|
|
|
show_billing=show_billing,
|
|
|
|
show_plans=show_plans,
|
2023-11-04 14:24:04 +01:00
|
|
|
sponsorship_pending=sponsorship_pending,
|
2023-11-15 22:44:24 +01:00
|
|
|
show_remote_billing=show_remote_billing,
|
2021-03-05 18:39:02 +01:00
|
|
|
)
|
2020-07-18 18:13:59 +02:00
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
)
|
2020-09-27 06:49:16 +02:00
|
|
|
else:
|
2020-07-18 18:13:59 +02:00
|
|
|
return UserPermissionInfo(
|
|
|
|
color_scheme=UserProfile.COLOR_SCHEME_AUTOMATIC,
|
|
|
|
is_guest=False,
|
|
|
|
is_realm_admin=False,
|
|
|
|
is_realm_owner=False,
|
|
|
|
show_webathena=False,
|
|
|
|
)
|
2020-07-18 20:33:28 +02:00
|
|
|
|
|
|
|
|
|
|
|
def build_page_params_for_home_page_load(
|
|
|
|
request: HttpRequest,
|
2020-09-27 06:49:16 +02:00
|
|
|
user_profile: Optional[UserProfile],
|
2020-10-02 00:00:28 +02:00
|
|
|
realm: Realm,
|
2020-07-18 20:33:28 +02:00
|
|
|
insecure_desktop_app: bool,
|
2023-06-30 00:50:04 +02:00
|
|
|
narrow: List[NarrowTerm],
|
2020-07-18 20:33:28 +02:00
|
|
|
narrow_stream: Optional[Stream],
|
2024-01-13 14:02:59 +01:00
|
|
|
narrow_topic_name: Optional[str],
|
2020-07-18 20:33:28 +02:00
|
|
|
needs_tutorial: bool,
|
2022-06-21 22:53:25 +02:00
|
|
|
) -> Tuple[int, Dict[str, object]]:
|
2020-07-18 20:33:28 +02:00
|
|
|
"""
|
|
|
|
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,
|
2021-01-04 16:21:54 +01:00
|
|
|
"stream_typing_notifications": True,
|
2021-07-28 16:00:58 +02:00
|
|
|
"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,
|
2023-11-28 18:24:43 +01:00
|
|
|
"user_list_incomplete": True,
|
2020-07-18 20:33:28 +02:00
|
|
|
}
|
|
|
|
|
2020-09-27 06:49:16 +02:00
|
|
|
if user_profile is not None:
|
2021-08-21 19:24:20 +02:00
|
|
|
client = RequestNotes.get_notes(request).client
|
2021-07-09 18:10:51 +02:00
|
|
|
assert client is not None
|
2024-02-13 02:56:26 +01:00
|
|
|
state_data = do_events_register(
|
2020-09-27 06:49:16 +02:00
|
|
|
user_profile,
|
2022-05-03 23:54:44 +02:00
|
|
|
realm,
|
2021-07-09 18:10:51 +02:00
|
|
|
client,
|
2020-09-27 06:49:16 +02:00
|
|
|
apply_markdown=True,
|
|
|
|
client_gravatar=True,
|
|
|
|
slim_presence=True,
|
|
|
|
client_capabilities=client_capabilities,
|
|
|
|
narrow=narrow,
|
|
|
|
include_streams=False,
|
|
|
|
)
|
2024-02-19 07:45:19 +01:00
|
|
|
queue_id = state_data["queue_id"]
|
2024-02-13 02:56:26 +01:00
|
|
|
default_language = state_data["user_settings"]["default_language"]
|
2020-09-27 06:49:16 +02:00
|
|
|
else:
|
2022-05-03 23:22:41 +02:00
|
|
|
# The spectator client will be fetching the /register response
|
2024-02-19 07:45:19 +01:00
|
|
|
# for spectators via the API.
|
|
|
|
state_data = None
|
|
|
|
queue_id = None
|
2022-05-03 23:22:41 +02:00
|
|
|
default_language = realm.default_language
|
2020-07-18 20:33:28 +02:00
|
|
|
|
2022-05-02 11:13:03 +02:00
|
|
|
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),
|
|
|
|
)
|
2020-07-18 20:33:28 +02:00
|
|
|
|
2022-05-02 11:13:03 +02:00
|
|
|
furthest_read_time = get_furthest_read_time(user_profile)
|
2021-02-12 08:19:30 +01:00
|
|
|
two_fa_enabled = settings.TWO_FACTOR_AUTHENTICATION_ENABLED and user_profile is not None
|
2021-06-14 12:38:43 +02:00
|
|
|
billing_info = get_billing_info(user_profile)
|
|
|
|
user_permission_info = get_user_permission_info(user_profile)
|
2020-07-18 20:33:28 +02:00
|
|
|
|
|
|
|
# Pass parameters to the client-side JavaScript code.
|
2021-03-25 22:35:45 +01:00
|
|
|
# These end up in a JavaScript Object named 'page_params'.
|
2024-02-16 22:56:36 +01:00
|
|
|
#
|
|
|
|
# Sync this with home_params_schema in base_page_params.ts.
|
2022-06-21 22:53:25 +02:00
|
|
|
page_params: Dict[str, object] = dict(
|
2024-02-16 22:56:36 +01:00
|
|
|
page_type="home",
|
2021-05-20 20:14:17 +02:00
|
|
|
## Server settings.
|
2020-07-18 20:33:28 +02:00
|
|
|
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,
|
2021-04-28 00:25:27 +02:00
|
|
|
# Only show marketing email settings if on Zulip Cloud
|
2021-06-14 09:07:47 +02:00
|
|
|
corporate_enabled=settings.CORPORATE_ENABLED,
|
2021-05-20 20:14:17 +02:00
|
|
|
## Misc. extra data.
|
2020-07-18 20:33:28 +02:00
|
|
|
language_list=get_language_list(),
|
|
|
|
needs_tutorial=needs_tutorial,
|
|
|
|
furthest_read_time=furthest_read_time,
|
|
|
|
bot_types=get_bot_types(user_profile),
|
|
|
|
two_fa_enabled=two_fa_enabled,
|
2021-06-14 12:38:43 +02:00
|
|
|
apps_page_url=get_apps_page_url(),
|
|
|
|
show_billing=billing_info.show_billing,
|
2023-11-15 22:44:24 +01:00
|
|
|
show_remote_billing=billing_info.show_remote_billing,
|
2021-06-14 12:38:43 +02:00
|
|
|
promote_sponsoring_zulip=promote_sponsoring_zulip_in_realm(realm),
|
|
|
|
show_plans=billing_info.show_plans,
|
2023-11-04 14:24:04 +01:00
|
|
|
sponsorship_pending=billing_info.sponsorship_pending,
|
2021-06-14 12:38:43 +02:00
|
|
|
show_webathena=user_permission_info.show_webathena,
|
2020-07-18 20:33:28 +02:00
|
|
|
# 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)),
|
2021-06-15 18:03:32 +02:00
|
|
|
is_spectator=user_profile is None,
|
|
|
|
# There is no event queue for spectators since
|
|
|
|
# events support for spectators is not implemented yet.
|
2020-09-27 06:49:16 +02:00
|
|
|
no_event_queue=user_profile is None,
|
sentry: Add frontend event monitoring.
Zulip already has integrations for server-side Sentry integration;
however, it has historically used the Zulip-specific `blueslip`
library for monitoring browser-side errors. However, the latter sends
errors to email, as well optionally to an internal `#errors` stream.
While this is sufficient for low volumes of users, and useful in that
it does not rely on outside services, at higher volumes it is very
difficult to do any analysis or filtering of the errors. Client-side
errors are exceptionally noisy, with many false positives due to
browser extensions or similar, so determining real real errors from a
stream of un-grouped emails or messages in a stream is quite
difficult.
Add a client-side Javascript sentry integration. To provide useful
backtraces, this requires extending the pre-deploy hooks to upload the
source-maps to Sentry. Additional keys are added to the non-public
API of `page_params` to control the DSN, realm identifier, and sample
rates.
2023-02-13 20:50:57 +01:00
|
|
|
server_sentry_dsn=settings.SENTRY_FRONTEND_DSN,
|
2020-07-18 20:33:28 +02:00
|
|
|
)
|
|
|
|
|
sentry: Add frontend event monitoring.
Zulip already has integrations for server-side Sentry integration;
however, it has historically used the Zulip-specific `blueslip`
library for monitoring browser-side errors. However, the latter sends
errors to email, as well optionally to an internal `#errors` stream.
While this is sufficient for low volumes of users, and useful in that
it does not rely on outside services, at higher volumes it is very
difficult to do any analysis or filtering of the errors. Client-side
errors are exceptionally noisy, with many false positives due to
browser extensions or similar, so determining real real errors from a
stream of un-grouped emails or messages in a stream is quite
difficult.
Add a client-side Javascript sentry integration. To provide useful
backtraces, this requires extending the pre-deploy hooks to upload the
source-maps to Sentry. Additional keys are added to the non-public
API of `page_params` to control the DSN, realm identifier, and sample
rates.
2023-02-13 20:50:57 +01:00
|
|
|
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
|
|
|
|
|
2024-02-13 02:56:26 +01:00
|
|
|
page_params["state_data"] = state_data
|
2020-07-18 20:33:28 +02:00
|
|
|
|
2024-02-19 07:45:19 +01:00
|
|
|
if narrow_stream is not None and state_data is not None:
|
2020-07-18 20:33:28 +02:00
|
|
|
# In narrow_stream context, initial pointer is just latest message
|
|
|
|
recipient = narrow_stream.recipient
|
2024-02-13 02:56:26 +01:00
|
|
|
state_data["max_message_id"] = -1
|
2023-08-30 21:19:37 +02:00
|
|
|
max_message = (
|
|
|
|
# Uses index: zerver_message_realm_recipient_id
|
|
|
|
Message.objects.filter(realm_id=realm.id, recipient=recipient)
|
|
|
|
.order_by("-id")
|
|
|
|
.only("id")
|
|
|
|
.first()
|
|
|
|
)
|
2023-08-30 18:55:46 +02:00
|
|
|
if max_message:
|
2024-02-13 02:56:26 +01:00
|
|
|
state_data["max_message_id"] = max_message.id
|
2020-07-18 20:33:28 +02:00
|
|
|
page_params["narrow_stream"] = narrow_stream.name
|
2024-01-13 14:02:59 +01:00
|
|
|
if narrow_topic_name is not None:
|
|
|
|
page_params["narrow_topic"] = narrow_topic_name
|
2023-06-30 00:50:04 +02:00
|
|
|
page_params["narrow"] = [
|
|
|
|
dict(operator=term.operator, operand=term.operand) for term in narrow
|
|
|
|
]
|
2024-02-13 02:56:26 +01:00
|
|
|
assert isinstance(state_data["user_settings"], dict)
|
|
|
|
state_data["user_settings"]["enable_desktop_notifications"] = False
|
2020-07-18 20:33:28 +02:00
|
|
|
|
2020-09-24 13:04:54 +02:00
|
|
|
page_params["translation_data"] = get_language_translation_data(request_language)
|
2020-07-18 20:33:28 +02:00
|
|
|
|
2022-02-15 10:58:53 +01:00
|
|
|
if user_profile is None:
|
|
|
|
# Get rendered version of realm description which is displayed in right
|
|
|
|
# sidebar for spectator.
|
2022-05-05 06:50:20 +02:00
|
|
|
page_params["realm_rendered_description"] = get_realm_rendered_description(realm)
|
2022-05-02 11:13:03 +02:00
|
|
|
page_params["language_cookie_name"] = settings.LANGUAGE_COOKIE_NAME
|
2022-02-15 10:58:53 +01:00
|
|
|
|
2024-02-19 07:45:19 +01:00
|
|
|
return queue_id, page_params
|