api: Add a monotonic integer "feature level" for non-webapp clients.

The purpose is to provide a way for (non-webapp) clients,
like the mobile and terminal apps, to tell whether the
server it's talking to is new enough to support a given
API feature -- in particular a way that

* is finer-grained than release numbers, so that for
features developed after e.g. 2.1.0 we can use them
immediately on servers deployed from master (like
chat.zulip.org and zulipchat.com) without waiting the
months until a 2.2 release;

* is reliable, unlike e.g. looking at the number of
commits since a release;

* doesn't lead to a growing bag of named feature flags
which the server has to go on sending forever.

Tweaked by tabbott to extend the documentation.

Closes #14618.
This commit is contained in:
Hashir Sarwar 2020-04-20 03:57:28 +05:00 committed by Tim Abbott
parent 0de77cabb0
commit e3b90a5ec8
7 changed files with 36 additions and 3 deletions

View File

@ -36,6 +36,16 @@ Fetch global settings for a Zulip server.
* `authentication_methods`: object in which each key-value pair in the object * `authentication_methods`: object in which each key-value pair in the object
indicates whether the authentication method is enabled on this server. indicates whether the authentication method is enabled on this server.
* `zulip_version`: the version of Zulip running in the server. * `zulip_version`: the version of Zulip running in the server.
* `zulip_feature_level`: an integer indicatating what features are
available on the server. The feature level increases monotonically;
a value of N means the server supports all API features introduced
before feature level N. This is designed to provide a simple way
for mobile apps to decide whether the server supports a given
feature or API change.
**Changes**. New in Zulip 2.2. We recommend using an implied value
of 0 for Zulip servers that do not send this field.
* `push_notifications_enabled`: whether mobile/push notifications are enabled. * `push_notifications_enabled`: whether mobile/push notifications are enabled.
* `is_incompatible`: whether the Zulip client that has sent a request to * `is_incompatible`: whether the Zulip client that has sent a request to
this endpoint is deemed incompatible with the server. this endpoint is deemed incompatible with the server.

View File

@ -21,6 +21,16 @@ LATEST_DESKTOP_VERSION = "5.0.0"
DESKTOP_MINIMUM_VERSION = "5.0.0" DESKTOP_MINIMUM_VERSION = "5.0.0"
DESKTOP_WARNING_VERSION = "5.0.0" DESKTOP_WARNING_VERSION = "5.0.0"
# Bump the API_FEATURE_LEVEL whenever an API change is made
# that clients might want to condition on. If we forget at
# the time we make the change, then bump it later as soon
# as we notice; clients using API_FEATURE_LEVEL will just not
# use the new feature/API until the bump.
#
# Changes should be accompanied by documentation explaining what the new
# level means in templates/zerver/api/server-settings.md.
API_FEATURE_LEVEL = 1
# 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
# the major version to indicate that folks should provision in both # the major version to indicate that folks should provision in both

View File

@ -59,7 +59,7 @@ from zerver.models import (
get_default_stream_groups, CustomProfileField, Stream get_default_stream_groups, CustomProfileField, Stream
) )
from zproject.backends import email_auth_enabled, password_auth_enabled from zproject.backends import email_auth_enabled, password_auth_enabled
from version import ZULIP_VERSION from version import ZULIP_VERSION, API_FEATURE_LEVEL
from zerver.lib.external_accounts import DEFAULT_EXTERNAL_ACCOUNTS from zerver.lib.external_accounts import DEFAULT_EXTERNAL_ACCOUNTS
def add_realm_logo_fields(state: Dict[str, Any], realm: Realm) -> None: def add_realm_logo_fields(state: Dict[str, Any], realm: Realm) -> None:
@ -312,6 +312,7 @@ def fetch_initial_state_data(user_profile: UserProfile,
if want('zulip_version'): if want('zulip_version'):
state['zulip_version'] = ZULIP_VERSION state['zulip_version'] = ZULIP_VERSION
state['zulip_feature_level'] = API_FEATURE_LEVEL
return state return state

View File

@ -2450,6 +2450,15 @@ paths:
type: string type: string
description: | description: |
The version of Zulip running in the server. The version of Zulip running in the server.
zulip_feature_level:
type: integer
description: |
An integer to indicate the features added to the server.
The feature level increases monotonically; a value of N
means the server supports all API features introduced
before feature level N. This provides fine-grained
distinctions among development versions in between major
releases.
push_notifications_enabled: push_notifications_enabled:
type: boolean type: boolean
description: | description: |

View File

@ -38,7 +38,7 @@ from zerver.lib.email_validation import get_realm_email_validator, \
from zerver.lib.exceptions import RateLimited from zerver.lib.exceptions import RateLimited
from zerver.lib.mobile_auth_otp import otp_decrypt_api_key from zerver.lib.mobile_auth_otp import otp_decrypt_api_key
from zerver.lib.validator import validate_login_email, \ from zerver.lib.validator import validate_login_email, \
check_bool, check_dict_only, check_list, check_string, Validator check_bool, check_dict_only, check_list, check_string, check_int, Validator
from zerver.lib.rate_limiter import add_ratelimit_rule, remove_ratelimit_rule from zerver.lib.rate_limiter import add_ratelimit_rule, remove_ratelimit_rule
from zerver.lib.request import JsonableError from zerver.lib.request import JsonableError
from zerver.lib.storage import static_path from zerver.lib.storage import static_path
@ -2552,6 +2552,7 @@ class FetchAuthBackends(ZulipTestCase):
('require_email_format_usernames', check_bool), ('require_email_format_usernames', check_bool),
('realm_uri', check_string), ('realm_uri', check_string),
('zulip_version', check_string), ('zulip_version', check_string),
('zulip_feature_level', check_int),
('push_notifications_enabled', check_bool), ('push_notifications_enabled', check_bool),
('msg', check_string), ('msg', check_string),
('result', check_string), ('result', check_string),

View File

@ -224,6 +224,7 @@ class HomeTest(ZulipTestCase):
"warn_no_email", "warn_no_email",
"webpack_public_path", "webpack_public_path",
"wildcard_mentions_notify", "wildcard_mentions_notify",
"zulip_feature_level",
"zulip_version", "zulip_version",
] ]

View File

@ -45,7 +45,7 @@ from zproject.backends import password_auth_enabled, dev_auth_enabled, \
ldap_auth_enabled, ZulipLDAPConfigurationError, ZulipLDAPAuthBackend, \ ldap_auth_enabled, ZulipLDAPConfigurationError, ZulipLDAPAuthBackend, \
AUTH_BACKEND_NAME_MAP, auth_enabled_helper, saml_auth_enabled, SAMLAuthBackend, \ AUTH_BACKEND_NAME_MAP, auth_enabled_helper, saml_auth_enabled, SAMLAuthBackend, \
redirect_to_config_error, ZulipRemoteUserBackend, validate_otp_params redirect_to_config_error, ZulipRemoteUserBackend, validate_otp_params
from version import ZULIP_VERSION from version import ZULIP_VERSION, API_FEATURE_LEVEL
import jwt import jwt
import logging import logging
@ -946,6 +946,7 @@ def api_get_server_settings(request: HttpRequest) -> HttpResponse:
result = dict( result = dict(
authentication_methods=get_auth_backends_data(request), authentication_methods=get_auth_backends_data(request),
zulip_version=ZULIP_VERSION, zulip_version=ZULIP_VERSION,
zulip_feature_level=API_FEATURE_LEVEL,
push_notifications_enabled=push_notifications_enabled(), push_notifications_enabled=push_notifications_enabled(),
is_incompatible=check_server_incompatibility(request), is_incompatible=check_server_incompatibility(request),
) )