zulip/zerver/lib/exceptions.py

735 lines
20 KiB
Python
Raw Normal View History

from enum import Enum, auto
from typing import Any
from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _
from django_stubs_ext import StrPromise
from typing_extensions import override
class ErrorCode(Enum):
BAD_REQUEST = auto() # Generic name, from the name of HTTP 400.
REQUEST_VARIABLE_MISSING = auto()
REQUEST_VARIABLE_INVALID = auto()
INVALID_JSON = auto()
BAD_IMAGE = auto()
REALM_UPLOAD_QUOTA = auto()
BAD_NARROW = auto()
CANNOT_DEACTIVATE_LAST_USER = auto()
MISSING_HTTP_EVENT_HEADER = auto()
STREAM_DOES_NOT_EXIST = auto()
UNAUTHORIZED_PRINCIPAL = auto()
UNSUPPORTED_WEBHOOK_EVENT_TYPE = auto()
ANOMALOUS_WEBHOOK_PAYLOAD = auto()
BAD_EVENT_QUEUE_ID = auto()
CSRF_FAILED = auto()
INVITATION_FAILED = auto()
INVALID_ZULIP_SERVER = auto()
INVALID_PUSH_DEVICE_TOKEN = auto()
INVALID_REMOTE_PUSH_DEVICE_TOKEN = auto()
INVALID_MARKDOWN_INCLUDE_STATEMENT = auto()
REQUEST_CONFUSING_VAR = auto()
INVALID_API_KEY = auto()
INVALID_ZOOM_TOKEN = auto()
UNAUTHENTICATED_USER = auto()
NONEXISTENT_SUBDOMAIN = auto()
RATE_LIMIT_HIT = auto()
USER_DEACTIVATED = auto()
REALM_DEACTIVATED = auto()
REMOTE_SERVER_DEACTIVATED = auto()
PASSWORD_AUTH_DISABLED = auto()
PASSWORD_RESET_REQUIRED = auto()
AUTHENTICATION_FAILED = auto()
UNAUTHORIZED = auto()
REQUEST_TIMEOUT = auto()
MOVE_MESSAGES_TIME_LIMIT_EXCEEDED = auto()
REACTION_ALREADY_EXISTS = auto()
REACTION_DOES_NOT_EXIST = auto()
SERVER_NOT_READY = auto()
MISSING_REMOTE_REALM = auto()
TOPIC_WILDCARD_MENTION_NOT_ALLOWED = auto()
STREAM_WILDCARD_MENTION_NOT_ALLOWED = auto()
REMOTE_BILLING_UNAUTHENTICATED_USER = auto()
REMOTE_REALM_SERVER_MISMATCH_ERROR = auto()
PUSH_NOTIFICATIONS_DISALLOWED = auto()
EXPECTATION_MISMATCH = auto()
SYSTEM_GROUP_REQUIRED = auto()
CANNOT_DEACTIVATE_GROUP_IN_USE = auto()
class JsonableError(Exception):
"""A standardized error format we can turn into a nice JSON HTTP response.
This class can be invoked in a couple ways.
* Easiest, but completely machine-unreadable:
raise JsonableError(_("No such widget: {}").format(widget_name))
The message may be passed through to clients and shown to a user,
so translation is required. Because the text will vary depending
on the user's language, it's not possible for code to distinguish
this error from others in a non-buggy way.
* Fully machine-readable, with an error code and structured data:
class NoSuchWidgetError(JsonableError):
code = ErrorCode.NO_SUCH_WIDGET
data_fields = ['widget_name']
def __init__(self, widget_name: str) -> None:
self.widget_name: str = widget_name
@staticmethod
def msg_format() -> str:
return _("No such widget: {widget_name}")
raise NoSuchWidgetError(widget_name)
Now both server and client code see a `widget_name` attribute
and an error code.
Subclasses may also override `http_status_code`.
"""
# Override this in subclasses, as needed.
python: Convert assignment type annotations to Python 3.6 style. This commit was split by tabbott; this piece covers the vast majority of files in Zulip, but excludes scripts/, tools/, and puppet/ to help ensure we at least show the right error messages for Xenial systems. We can likely further refine the remaining pieces with some testing. Generated by com2ann, with whitespace fixes and various manual fixes for runtime issues: - invoiced_through: Optional[LicenseLedger] = models.ForeignKey( + invoiced_through: Optional["LicenseLedger"] = models.ForeignKey( -_apns_client: Optional[APNsClient] = None +_apns_client: Optional["APNsClient"] = None - notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE) - signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE) + notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE) + signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE) - author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE) + author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE) - bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL) + bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL) - default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE) - default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE) + default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE) + default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE) -descriptors_by_handler_id: Dict[int, ClientDescriptor] = {} +descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {} -worker_classes: Dict[str, Type[QueueProcessingWorker]] = {} -queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {} +worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {} +queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {} -AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None +AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
code: ErrorCode = ErrorCode.BAD_REQUEST
# Override this in subclasses if providing structured data.
data_fields: list[str] = []
# Optionally override this in subclasses to return a different HTTP status,
# like 403 or 404.
python: Convert assignment type annotations to Python 3.6 style. This commit was split by tabbott; this piece covers the vast majority of files in Zulip, but excludes scripts/, tools/, and puppet/ to help ensure we at least show the right error messages for Xenial systems. We can likely further refine the remaining pieces with some testing. Generated by com2ann, with whitespace fixes and various manual fixes for runtime issues: - invoiced_through: Optional[LicenseLedger] = models.ForeignKey( + invoiced_through: Optional["LicenseLedger"] = models.ForeignKey( -_apns_client: Optional[APNsClient] = None +_apns_client: Optional["APNsClient"] = None - notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE) - signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE) + notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE) + signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE) - author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE) + author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE) - bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL) + bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL) - default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE) - default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE) + default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE) + default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE) -descriptors_by_handler_id: Dict[int, ClientDescriptor] = {} +descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {} -worker_classes: Dict[str, Type[QueueProcessingWorker]] = {} -queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {} +worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {} +queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {} -AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None +AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
http_status_code: int = 400
def __init__(self, msg: str | StrPromise) -> None:
# `_msg` is an implementation detail of `JsonableError` itself.
self._msg = msg
@staticmethod
def msg_format() -> str:
"""Override in subclasses. Gets the items in `data_fields` as format args.
This should return (a translation of) a string literal.
The reason it's not simply a class attribute is to allow
translation to work.
"""
# Secretly this gets one more format arg not in `data_fields`: `_msg`.
# That's for the sake of the `JsonableError` base logic itself, for
# the simplest form of use where we just get a plain message string
# at construction time.
return "{_msg}"
@property
def extra_headers(self) -> dict[str, Any]:
return {}
#
# Infrastructure -- not intended to be overridden in subclasses.
#
@property
def msg(self) -> str:
format_data = dict(
((f, getattr(self, f)) for f in self.data_fields), _msg=getattr(self, "_msg", None)
)
return self.msg_format().format(**format_data)
@property
def data(self) -> dict[str, Any]:
return dict(((f, getattr(self, f)) for f in self.data_fields), code=self.code.name)
@override
def __str__(self) -> str:
return self.msg
class UnauthorizedError(JsonableError):
code: ErrorCode = ErrorCode.UNAUTHORIZED
http_status_code: int = 401
def __init__(self, msg: str | None = None, www_authenticate: str | None = None) -> None:
if msg is None:
msg = _("Not logged in: API authentication or user session required")
super().__init__(msg)
if www_authenticate is None:
self.www_authenticate = 'Basic realm="zulip"'
elif www_authenticate == "session":
self.www_authenticate = 'Session realm="zulip"'
else:
raise AssertionError("Invalid www_authenticate value!")
@property
@override
def extra_headers(self) -> dict[str, Any]:
extra_headers_dict = super().extra_headers
extra_headers_dict["WWW-Authenticate"] = self.www_authenticate
return extra_headers_dict
class StreamDoesNotExistError(JsonableError):
code = ErrorCode.STREAM_DOES_NOT_EXIST
data_fields = ["stream"]
def __init__(self, stream: str) -> None:
self.stream = stream
@staticmethod
@override
def msg_format() -> str:
return _("Channel '{stream}' does not exist")
class StreamWithIDDoesNotExistError(JsonableError):
code = ErrorCode.STREAM_DOES_NOT_EXIST
data_fields = ["stream_id"]
def __init__(self, stream_id: int) -> None:
self.stream_id = stream_id
@staticmethod
@override
def msg_format() -> str:
return _("Channel with ID '{stream_id}' does not exist")
class IncompatibleParametersError(JsonableError):
data_fields = ["parameters"]
def __init__(self, parameters: list[str]) -> None:
self.parameters = ", ".join(parameters)
@staticmethod
@override
def msg_format() -> str:
return _("Unsupported parameter combination: {parameters}")
class CannotDeactivateLastUserError(JsonableError):
code = ErrorCode.CANNOT_DEACTIVATE_LAST_USER
data_fields = ["is_last_owner", "entity"]
def __init__(self, is_last_owner: bool) -> None:
self.is_last_owner = is_last_owner
self.entity = _("organization owner") if is_last_owner else _("user")
@staticmethod
@override
def msg_format() -> str:
return _("Cannot deactivate the only {entity}.")
class InvalidMarkdownIncludeStatementError(JsonableError):
code = ErrorCode.INVALID_MARKDOWN_INCLUDE_STATEMENT
data_fields = ["include_statement"]
def __init__(self, include_statement: str) -> None:
self.include_statement = include_statement
@staticmethod
@override
def msg_format() -> str:
return _("Invalid Markdown include statement: {include_statement}")
class RateLimitedError(JsonableError):
code = ErrorCode.RATE_LIMIT_HIT
http_status_code = 429
def __init__(self, secs_to_freedom: float | None = None) -> None:
self.secs_to_freedom = secs_to_freedom
@staticmethod
@override
def msg_format() -> str:
return _("API usage exceeded rate limit")
@property
@override
def extra_headers(self) -> dict[str, Any]:
extra_headers_dict = super().extra_headers
if self.secs_to_freedom is not None:
extra_headers_dict["Retry-After"] = self.secs_to_freedom
return extra_headers_dict
@property
@override
def data(self) -> dict[str, Any]:
data_dict = super().data
data_dict["retry-after"] = self.secs_to_freedom
return data_dict
class InvalidJSONError(JsonableError):
code = ErrorCode.INVALID_JSON
@staticmethod
@override
def msg_format() -> str:
return _("Malformed JSON")
class OrganizationMemberRequiredError(JsonableError):
code: ErrorCode = ErrorCode.UNAUTHORIZED_PRINCIPAL
def __init__(self) -> None:
pass
@staticmethod
@override
def msg_format() -> str:
return _("Must be an organization member")
class OrganizationAdministratorRequiredError(JsonableError):
python: Convert assignment type annotations to Python 3.6 style. This commit was split by tabbott; this piece covers the vast majority of files in Zulip, but excludes scripts/, tools/, and puppet/ to help ensure we at least show the right error messages for Xenial systems. We can likely further refine the remaining pieces with some testing. Generated by com2ann, with whitespace fixes and various manual fixes for runtime issues: - invoiced_through: Optional[LicenseLedger] = models.ForeignKey( + invoiced_through: Optional["LicenseLedger"] = models.ForeignKey( -_apns_client: Optional[APNsClient] = None +_apns_client: Optional["APNsClient"] = None - notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE) - signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE) + notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE) + signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE) - author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE) + author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE) - bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL) + bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL) - default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE) - default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE) + default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE) + default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE) -descriptors_by_handler_id: Dict[int, ClientDescriptor] = {} +descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {} -worker_classes: Dict[str, Type[QueueProcessingWorker]] = {} -queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {} +worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {} +queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {} -AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None +AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
code: ErrorCode = ErrorCode.UNAUTHORIZED_PRINCIPAL
def __init__(self) -> None:
pass
@staticmethod
@override
def msg_format() -> str:
return _("Must be an organization administrator")
class OrganizationOwnerRequiredError(JsonableError):
code: ErrorCode = ErrorCode.UNAUTHORIZED_PRINCIPAL
def __init__(self) -> None:
pass
@staticmethod
@override
def msg_format() -> str:
return _("Must be an organization owner")
class AuthenticationFailedError(JsonableError):
# Generic class for authentication failures
code: ErrorCode = ErrorCode.AUTHENTICATION_FAILED
http_status_code = 401
def __init__(self) -> None:
pass
@staticmethod
@override
def msg_format() -> str:
return _("Your username or password is incorrect")
class UserDeactivatedError(AuthenticationFailedError):
code: ErrorCode = ErrorCode.USER_DEACTIVATED
@staticmethod
@override
def msg_format() -> str:
return _("Account is deactivated")
class RealmDeactivatedError(AuthenticationFailedError):
code: ErrorCode = ErrorCode.REALM_DEACTIVATED
@staticmethod
@override
def msg_format() -> str:
return _("This organization has been deactivated")
class RemoteServerDeactivatedError(AuthenticationFailedError):
code: ErrorCode = ErrorCode.REALM_DEACTIVATED
@staticmethod
@override
def msg_format() -> str:
return _(
"The mobile push notification service registration for your server has been deactivated"
)
class PasswordAuthDisabledError(AuthenticationFailedError):
code: ErrorCode = ErrorCode.PASSWORD_AUTH_DISABLED
@staticmethod
@override
def msg_format() -> str:
return _("Password authentication is disabled in this organization")
class PasswordResetRequiredError(AuthenticationFailedError):
code: ErrorCode = ErrorCode.PASSWORD_RESET_REQUIRED
@staticmethod
@override
def msg_format() -> str:
return _("Your password has been disabled and needs to be reset")
class MarkdownRenderingError(Exception):
pass
class InvalidAPIKeyError(JsonableError):
code = ErrorCode.INVALID_API_KEY
http_status_code = 401
def __init__(self) -> None:
pass
@staticmethod
@override
def msg_format() -> str:
return _("Invalid API key")
class InvalidAPIKeyFormatError(InvalidAPIKeyError):
@staticmethod
@override
def msg_format() -> str:
return _("Malformed API key")
class WebhookError(JsonableError):
"""
Intended as a generic exception raised by specific webhook
integrations. This class is subclassed by more specific exceptions
such as UnsupportedWebhookEventTypeError and AnomalousWebhookPayloadError.
"""
data_fields = ["webhook_name"]
def __init__(self) -> None:
# webhook_name is often set by decorators such as webhook_view
# in zerver/decorator.py
self.webhook_name = "(unknown)"
class UnsupportedWebhookEventTypeError(WebhookError):
"""Intended as an exception for event formats that we know the
third-party service generates but which Zulip doesn't support /
generate a message for.
Exceptions where we cannot parse the event type, possibly because
the event isn't actually from the service in question, should
raise AnomalousWebhookPayloadError.
"""
code = ErrorCode.UNSUPPORTED_WEBHOOK_EVENT_TYPE
http_status_code = 200
data_fields = ["webhook_name", "event_type"]
def __init__(self, event_type: str | None) -> None:
super().__init__()
self.event_type = event_type
@staticmethod
@override
def msg_format() -> str:
return _(
"The '{event_type}' event isn't currently supported by the {webhook_name} webhook; ignoring"
)
class AnomalousWebhookPayloadError(WebhookError):
"""Intended as an exception for incoming webhook requests that we
cannot recognize as having been generated by the service in
question. (E.g. because someone pointed a Jira server at the
GitHub integration URL).
If we can parse the event but don't support it, use
UnsupportedWebhookEventTypeError.
"""
code = ErrorCode.ANOMALOUS_WEBHOOK_PAYLOAD
@staticmethod
@override
def msg_format() -> str:
return _("Unable to parse request: Did {webhook_name} generate this event?")
class MissingAuthenticationError(JsonableError):
code = ErrorCode.UNAUTHENTICATED_USER
http_status_code = 401
def __init__(self) -> None:
pass
# No msg_format is defined since this exception is caught and
# converted into json_unauthorized in Zulip's middleware.
class RemoteBillingAuthenticationError(JsonableError):
# We want this as a distinct class from MissingAuthenticationError,
# as we don't want the json_unauthorized conversion mechanism to apply
# to this.
code = ErrorCode.REMOTE_BILLING_UNAUTHENTICATED_USER
http_status_code = 401
def __init__(self) -> None:
pass
@staticmethod
@override
def msg_format() -> str:
return _("User not authenticated")
class InvalidSubdomainError(JsonableError):
code = ErrorCode.NONEXISTENT_SUBDOMAIN
http_status_code = 404
def __init__(self) -> None:
pass
@staticmethod
@override
def msg_format() -> str:
return _("Invalid subdomain")
class ZephyrMessageAlreadySentError(Exception):
def __init__(self, message_id: int) -> None:
self.message_id = message_id
class InvitationError(JsonableError):
code = ErrorCode.INVITATION_FAILED
data_fields = [
"errors",
"sent_invitations",
"license_limit_reached",
"daily_limit_reached",
]
def __init__(
self,
msg: str,
errors: list[tuple[str, str, bool]],
sent_invitations: bool,
license_limit_reached: bool = False,
daily_limit_reached: bool = False,
) -> None:
self._msg: str = msg
self.errors: list[tuple[str, str, bool]] = errors
self.sent_invitations: bool = sent_invitations
self.license_limit_reached: bool = license_limit_reached
self.daily_limit_reached: bool = daily_limit_reached
2021-07-04 08:45:34 +02:00
class DirectMessageInitiationError(JsonableError):
def __init__(self) -> None:
pass
@staticmethod
@override
def msg_format() -> str:
return _("You do not have permission to initiate direct message conversations.")
class DirectMessagePermissionError(JsonableError):
def __init__(self, is_nobody_group: bool) -> None:
if is_nobody_group:
msg = _("Direct messages are disabled in this organization.")
else:
msg = _("This conversation does not include any users who can authorize it.")
super().__init__(msg)
2021-07-04 08:45:34 +02:00
class AccessDeniedError(JsonableError):
http_status_code = 403
def __init__(self) -> None:
pass
@staticmethod
@override
2021-07-04 08:45:34 +02:00
def msg_format() -> str:
return _("Access denied")
2021-07-04 08:52:23 +02:00
class ResourceNotFoundError(JsonableError):
http_status_code = 404
class ValidationFailureError(JsonableError):
# This class translations a Django ValidationError into a
# Zulip-style JsonableError, sending back just the first error for
# consistency of API.
data_fields = ["errors"]
def __init__(self, error: ValidationError) -> None:
super().__init__(error.messages[0])
self.errors = error.message_dict
class MessageMoveError(JsonableError):
code = ErrorCode.MOVE_MESSAGES_TIME_LIMIT_EXCEEDED
data_fields = [
"first_message_id_allowed_to_move",
"total_messages_in_topic",
"total_messages_allowed_to_move",
]
def __init__(
self,
first_message_id_allowed_to_move: int,
total_messages_in_topic: int,
total_messages_allowed_to_move: int,
) -> None:
self.first_message_id_allowed_to_move = first_message_id_allowed_to_move
self.total_messages_in_topic = total_messages_in_topic
self.total_messages_allowed_to_move = total_messages_allowed_to_move
@staticmethod
@override
def msg_format() -> str:
return _(
"You only have permission to move the {total_messages_allowed_to_move}/{total_messages_in_topic} most recent messages in this topic."
)
class ReactionExistsError(JsonableError):
code = ErrorCode.REACTION_ALREADY_EXISTS
def __init__(self) -> None:
pass
@staticmethod
@override
def msg_format() -> str:
return _("Reaction already exists.")
class ReactionDoesNotExistError(JsonableError):
code = ErrorCode.REACTION_DOES_NOT_EXIST
def __init__(self) -> None:
pass
@staticmethod
@override
def msg_format() -> str:
return _("Reaction doesn't exist.")
api: Add new typed_endpoint decorators. The goal of typed_endpoint is to replicate most features supported by has_request_variables, and to improve on top of it. There are some unresolved issues that we don't plan to work on currently. For example, typed_endpoint does not support ignored_parameters_supported for 400 responses, and it does not run validators on path-only arguments. Unlike has_request_variables, typed_endpoint supports error handling by processing validation errors from Pydantic. Most features supported by has_request_variables are supported by typed_endpoint in various ways. To define a function, use a syntax like this with Annotated if there is any metadata you want to associate with a parameter, do note that parameters that are not keyword-only are ignored from the request: ``` @typed_endpoint def view( request: HttpRequest, user_profile: UserProfile, *, foo: Annotated[int, ApiParamConfig(path_only=True)], bar: Json[int], other: Annotated[ Json[int], ApiParamConfig( whence="lorem", documentation_status=NTENTIONALLY_UNDOCUMENTED ) ] = 10, ) -> HttpResponse: .... ``` There are also some shorthands for the commonly used annotated types, which are encouraged when applicable for better readability and less typing: ``` WebhookPayload = Annotated[Json[T], ApiParamConfig(argument_type_is_body=True)] PathOnly = Annotated[T, ApiParamConfig(path_only=True)] ``` Then the view function above can be rewritten as: ``` @typed_endpoint def view( request: HttpRequest, user_profile: UserProfile, *, foo: PathOnly[int], bar: Json[int], other: Annotated[ Json[int], ApiParamConfig( whence="lorem", documentation_status=INTENTIONALLY_UNDOCUMENTED ) ] = 10, ) -> HttpResponse: .... ``` There are some intentional restrictions: - A single parameter cannot have more than one ApiParamConfig - Path-only parameters cannot have default values - argument_type_is_body is incompatible with whence - Arguments of name "request", "user_profile", "args", and "kwargs" and etc. are ignored by typed_endpoint. - positional-only arguments are not supported by typed_endpoint. Only keyword-only parameters are expected to be parsed from the request. - Pydantic's strict mode is always enabled, because we don't want to coerce input parsed from JSON into other types unnecessarily. - Using strict mode all the time also means that we should always use Json[int] instead of int, because it is only possible for the request to have data of type str, and a type annotation of int will always reject such data. typed_endpoint's handling of ignored_parameters_unsupported is mostly identical to that of has_request_variables.
2023-07-28 08:34:04 +02:00
class ApiParamValidationError(JsonableError):
def __init__(self, msg: str, error_type: str) -> None:
super().__init__(msg)
self.error_type = error_type
class ServerNotReadyError(JsonableError):
code = ErrorCode.SERVER_NOT_READY
http_status_code = 500
class RemoteRealmServerMismatchError(JsonableError): # nocoverage
code = ErrorCode.REMOTE_REALM_SERVER_MISMATCH_ERROR
http_status_code = 403
def __init__(self) -> None:
pass
@staticmethod
@override
def msg_format() -> str:
return _(
"Your organization is registered to a different Zulip server. Please contact Zulip support for assistance in resolving this issue."
)
class MissingRemoteRealmError(JsonableError): # nocoverage
code: ErrorCode = ErrorCode.MISSING_REMOTE_REALM
http_status_code = 403
def __init__(self) -> None:
pass
@staticmethod
@override
def msg_format() -> str:
return _("Organization not registered")
class StreamWildcardMentionNotAllowedError(JsonableError):
code: ErrorCode = ErrorCode.STREAM_WILDCARD_MENTION_NOT_ALLOWED
def __init__(self) -> None:
pass
@staticmethod
@override
def msg_format() -> str:
return _("You do not have permission to use channel wildcard mentions in this channel.")
class TopicWildcardMentionNotAllowedError(JsonableError):
code: ErrorCode = ErrorCode.TOPIC_WILDCARD_MENTION_NOT_ALLOWED
def __init__(self) -> None:
pass
@staticmethod
@override
def msg_format() -> str:
return _("You do not have permission to use topic wildcard mentions in this topic.")
class PreviousSettingValueMismatchedError(JsonableError):
code: ErrorCode = ErrorCode.EXPECTATION_MISMATCH
def __init__(self) -> None:
pass
@staticmethod
@override
def msg_format() -> str:
return _("'old' value does not match the expected value.")
class SystemGroupRequiredError(JsonableError):
code = ErrorCode.SYSTEM_GROUP_REQUIRED
data_fields = ["setting_name"]
def __init__(self, setting_name: str) -> None:
self.setting_name = setting_name
@staticmethod
@override
def msg_format() -> str:
return _("'{setting_name}' must be a system user group.")
class IncompatibleParameterValuesError(JsonableError):
data_fields = ["first_parameter", "second_parameter"]
def __init__(self, first_parameter: str, second_parameter: str) -> None:
self.first_parameter = first_parameter
self.second_parameter = second_parameter
@staticmethod
@override
def msg_format() -> str:
return _("Incompatible values for '{first_parameter}' and '{second_parameter}'.")
class CannotDeactivateGroupInUseError(JsonableError):
code = ErrorCode.CANNOT_DEACTIVATE_GROUP_IN_USE
data_fields = ["objections"]
def __init__(
self,
objections: list[dict[str, Any]],
) -> None:
self.objections = objections
@staticmethod
@override
def msg_format() -> str:
return _("Cannot deactivate user group in use.")