Files
zulip/zerver/lib/exceptions.py
Anders Kaseorg 4d04fa3118 compose: Rewrite Zoom video call integration to use OAuth.
This reimplements our Zoom video call integration to use an OAuth
application.  In addition to providing a cleaner setup experience,
especially on zulipchat.com where the server administrators can have
done the app registration already, it also fixes the limitation of the
previous integration that it could only have one call active at a time
when set up with typical Zoom API keys.

Fixes #11672.

Co-authored-by: Marco Burstein <marco@marco.how>
Co-authored-by: Tim Abbott <tabbott@zulipchat.com>
Signed-off-by: Anders Kaseorg <andersk@mit.edu>
2020-06-03 16:39:12 -07:00

235 lines
7.2 KiB
Python

from enum import Enum
from typing import Any, Dict, List, Type, TypeVar, Optional
from typing_extensions import NoReturn
from django.utils.translation import ugettext as _
T = TypeVar("T", bound="AbstractEnum")
class AbstractEnum(Enum):
'''An enumeration whose members are used strictly for their names.'''
def __new__(cls: Type[T]) -> T:
obj = object.__new__(cls)
obj._value_ = len(cls.__members__) + 1
return obj
# Override all the `Enum` methods that use `_value_`.
def __repr__(self) -> str:
return str(self) # nocoverage
def value(self) -> None:
raise AssertionError("Not implemented")
def __reduce_ex__(self, proto: object) -> NoReturn:
raise AssertionError("Not implemented")
class ErrorCode(AbstractEnum):
BAD_REQUEST = () # Generic name, from the name of HTTP 400.
REQUEST_VARIABLE_MISSING = ()
REQUEST_VARIABLE_INVALID = ()
INVALID_JSON = ()
BAD_IMAGE = ()
REALM_UPLOAD_QUOTA = ()
BAD_NARROW = ()
CANNOT_DEACTIVATE_LAST_USER = ()
MISSING_HTTP_EVENT_HEADER = ()
STREAM_DOES_NOT_EXIST = ()
UNAUTHORIZED_PRINCIPAL = ()
UNEXPECTED_WEBHOOK_EVENT_TYPE = ()
BAD_EVENT_QUEUE_ID = ()
CSRF_FAILED = ()
INVITATION_FAILED = ()
INVALID_ZULIP_SERVER = ()
INVALID_MARKDOWN_INCLUDE_STATEMENT = ()
REQUEST_CONFUSING_VAR = ()
INVALID_API_KEY = ()
INVALID_ZOOM_TOKEN = ()
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.
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.
http_status_code: int = 400
def __init__(self, msg: str) -> None:
# `_msg` is an implementation detail of `JsonableError` itself.
self._msg: str = 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}'
#
# 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)
def to_json(self) -> Dict[str, Any]:
d = {'result': 'error', 'msg': self.msg}
d.update(self.data)
return d
def __str__(self) -> str:
return self.msg
class StreamDoesNotExistError(JsonableError):
code = ErrorCode.STREAM_DOES_NOT_EXIST
data_fields = ['stream']
def __init__(self, stream: str) -> None:
self.stream = stream
@staticmethod
def msg_format() -> str:
return _("Stream '{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
def msg_format() -> str:
return _("Stream with ID '{stream_id}' does not exist")
class CannotDeactivateLastUserError(JsonableError):
code = ErrorCode.CANNOT_DEACTIVATE_LAST_USER
data_fields = ['is_last_admin', 'entity']
def __init__(self, is_last_admin: bool) -> None:
self.is_last_admin = is_last_admin
self.entity = _("organization administrator") if is_last_admin else _("user")
@staticmethod
def msg_format() -> str:
return _("Cannot deactivate the only {entity}.")
class InvalidMarkdownIncludeStatement(JsonableError):
code = ErrorCode.INVALID_MARKDOWN_INCLUDE_STATEMENT
data_fields = ['include_statement']
def __init__(self, include_statement: str) -> None:
self.include_statement = include_statement
@staticmethod
def msg_format() -> str:
return _("Invalid markdown include statement: {include_statement}")
class RateLimited(Exception):
def __init__(self, msg: str="") -> None:
super().__init__(msg)
class InvalidJSONError(JsonableError):
code = ErrorCode.INVALID_JSON
@staticmethod
def msg_format() -> str:
return _("Malformed JSON")
class OrganizationAdministratorRequired(JsonableError):
code: ErrorCode = ErrorCode.UNAUTHORIZED_PRINCIPAL
ADMIN_REQUIRED_MESSAGE = _("Must be an organization administrator")
def __init__(self) -> None:
super().__init__(self.ADMIN_REQUIRED_MESSAGE)
@staticmethod
def msg_format() -> str:
return OrganizationAdministratorRequired.ADMIN_REQUIRED_MESSAGE
class BugdownRenderingException(Exception):
pass
class InvalidAPIKeyError(JsonableError):
code = ErrorCode.INVALID_API_KEY
http_status_code = 401
def __init__(self) -> None:
pass
@staticmethod
def msg_format() -> str:
return _("Invalid API key")
class InvalidAPIKeyFormatError(InvalidAPIKeyError):
@staticmethod
def msg_format() -> str:
return _("Malformed API key")
class UnexpectedWebhookEventType(JsonableError):
code = ErrorCode.UNEXPECTED_WEBHOOK_EVENT_TYPE
data_fields = ['webhook_name', 'event_type']
def __init__(self, webhook_name: str, event_type: Optional[str]) -> None:
self.webhook_name = webhook_name
self.event_type = event_type
@staticmethod
def msg_format() -> str:
return _("The '{event_type}' event isn't currently supported by the {webhook_name} webhook")