예제 #1
0
def process_success_response(event: Dict[str, Any], service_handler: Any,
                             response: Response) -> None:
    try:
        response_json = json.loads(response.text)
    except json.JSONDecodeError:
        raise JsonableError(_("Invalid JSON in response"))

    if not isinstance(response_json, dict):
        raise JsonableError(_("Invalid response format"))

    success_data = service_handler.process_success(response_json)

    if success_data is None:
        return

    content = success_data.get("content")

    if content is None or content.strip() == "":
        return

    widget_content = success_data.get("widget_content")
    bot_id = event["user_profile_id"]
    message_info = event["message"]
    response_data = dict(content=content, widget_content=widget_content)
    send_response_message(bot_id=bot_id,
                          message_info=message_info,
                          response_data=response_data)
예제 #2
0
def process_success_response(event: Dict[str, Any], service_handler: Any,
                             response: Response) -> None:
    try:
        response_json = json.loads(response.text)
    except json.JSONDecodeError:
        raise JsonableError(_("Invalid JSON in response"))

    if response_json == "":
        # Versions of zulip_botserver before 2021-05 used
        # json.dumps("") as their "no response required" success
        # response; handle that for backwards-compatibility.
        return

    if not isinstance(response_json, dict):
        raise JsonableError(_("Invalid response format"))

    success_data = service_handler.process_success(response_json)

    if success_data is None:
        return

    content = success_data.get("content")

    if content is None or content.strip() == "":
        return

    widget_content = success_data.get("widget_content")
    bot_id = event["user_profile_id"]
    message_info = event["message"]
    response_data = dict(content=content, widget_content=widget_content)
    send_response_message(bot_id=bot_id,
                          message_info=message_info,
                          response_data=response_data)
예제 #3
0
def update_message_flags(request,
                         user_profile,
                         messages=REQ('messages',
                                      validator=check_list(check_int)),
                         operation=REQ('op'),
                         flag=REQ('flag'),
                         all=REQ('all', validator=check_bool, default=False),
                         stream_name=REQ('stream_name', default=None),
                         topic_name=REQ('topic_name', default=None)):

    request._log_data["extra"] = "[%s %s]" % (operation, flag)
    stream = None
    if stream_name is not None:
        stream = get_stream(stream_name, user_profile.realm)
        if not stream:
            raise JsonableError(_('No such stream \'%s\'') % (stream_name, ))
        if topic_name:
            topic_exists = UserMessage.objects.filter(
                user_profile=user_profile,
                message__recipient__type_id=stream.id,
                message__recipient__type=Recipient.STREAM,
                message__subject__iexact=topic_name).exists()
            if not topic_exists:
                raise JsonableError(_('No such topic \'%s\'') % (topic_name, ))
    do_update_message_flags(user_profile, operation, flag, messages, all,
                            stream, topic_name)
    return json_success({'result': 'success', 'messages': messages, 'msg': ''})
예제 #4
0
def fetch_events(query):
    queue_id = query["queue_id"]
    dont_block = query["dont_block"]
    last_event_id = query["last_event_id"]
    user_profile_id = query["user_profile_id"]
    new_queue_data = query.get("new_queue_data")
    user_profile_email = query["user_profile_email"]
    client_type_name = query["client_type_name"]
    handler_id = query["handler_id"]

    try:
        was_connected = False
        orig_queue_id = queue_id
        extra_log_data = ""
        if queue_id is None:
            if dont_block:
                client = allocate_client_descriptor(new_queue_data)
                queue_id = client.event_queue.id
            else:
                raise JsonableError("Missing 'queue_id' argument")
        else:
            if last_event_id is None:
                raise JsonableError("Missing 'last_event_id' argument")
            client = get_client_descriptor(queue_id)
            if client is None:
                raise JsonableError("Bad event queue id: %s" % (queue_id, ))
            if user_profile_id != client.user_profile_id:
                raise JsonableError(
                    "You are not authorized to get events from this queue")
            client.event_queue.prune(last_event_id)
            was_connected = client.finish_current_handler()

        if not client.event_queue.empty() or dont_block:
            response = dict(events=client.event_queue.contents(),
                            handler_id=handler_id)
            if orig_queue_id is None:
                response['queue_id'] = queue_id
            extra_log_data = "[%s/%s]" % (queue_id, len(response["events"]))
            if was_connected:
                extra_log_data += " [was connected]"
            return dict(type="response",
                        response=response,
                        extra_log_data=extra_log_data)

        # After this point, dont_block=False, the queue is empty, and we
        # have a pre-existing queue, so we wait for new events.
        if was_connected:
            logging.info("Disconnected handler for queue %s (%s/%s)" %
                         (queue_id, user_profile_email, client_type_name))
    except JsonableError as e:
        if hasattr(e, 'to_json_error_msg') and callable(e.to_json_error_msg):
            return dict(type="error",
                        handler_id=handler_id,
                        message=e.to_json_error_msg())
        raise e

    client.connect_handler(handler_id, client_type_name)
    return dict(type="async")
예제 #5
0
    def authenticate_client(self, msg: Dict[str, Any]) -> None:
        if self.authenticated:
            self.session.send_message({'req_id': msg['req_id'], 'type': 'response',
                                       'response': {'result': 'error',
                                                    'msg': 'Already authenticated'}})
            return

        user_profile = get_user_profile(self.browser_session_id)
        if user_profile is None:
            raise JsonableError(_('Unknown or missing session'))
        self.session.user_profile = user_profile

        if 'csrf_token' not in msg['request']:
            # Debugging code to help with understanding #6961
            logging.error("Invalid websockets auth request: %s" % (msg['request'],))
            raise JsonableError(_('CSRF token entry missing from request'))
        if not _compare_salted_tokens(msg['request']['csrf_token'], self.csrf_token):
            raise JsonableError(_('CSRF token does not match that in cookie'))

        if 'queue_id' not in msg['request']:
            raise JsonableError(_("Missing 'queue_id' argument"))

        queue_id = msg['request']['queue_id']
        client = get_client_descriptor(queue_id)
        if client is None:
            raise BadEventQueueIdError(queue_id)

        if user_profile.id != client.user_profile_id:
            raise JsonableError(_("You are not the owner of the queue with id '%s'") % (queue_id,))

        self.authenticated = True
        register_connection(queue_id, self)

        response = {'req_id': msg['req_id'], 'type': 'response',
                    'response': {'result': 'success', 'msg': ''}}

        status_inquiries = msg['request'].get('status_inquiries')
        if status_inquiries is not None:
            results = {}  # type: Dict[str, Dict[str, str]]
            for inquiry in status_inquiries:
                status = redis_client.hgetall(req_redis_key(inquiry))  # type: Dict[bytes, bytes]
                if len(status) == 0:
                    result = {'status': 'not_received'}
                elif b'response' not in status:
                    result = {'status': status[b'status'].decode('utf-8')}
                else:
                    result = {'status': status[b'status'].decode('utf-8'),
                              'response': ujson.loads(status[b'response'])}
                results[str(inquiry)] = result
            response['response']['status_inquiries'] = results

        self.session.send_message(response)
        ioloop = tornado.ioloop.IOLoop.instance()
        ioloop.remove_timeout(self.timeout_handle)
예제 #6
0
파일: streams.py 프로젝트: anujsrc/zulip
def get_subscription_or_die(stream_name, user_profile):
    stream = get_stream(stream_name, user_profile.realm)
    if not stream:
        raise JsonableError("Invalid stream %s" % (stream.name,))
    recipient = get_recipient(Recipient.STREAM, stream.id)
    subscription = Subscription.objects.filter(user_profile=user_profile,
                                               recipient=recipient, active=True)

    if not subscription.exists():
        raise JsonableError("Not subscribed to stream %s" % (stream_name,))

    return subscription
예제 #7
0
파일: streams.py 프로젝트: kevinwmx/zulip
def list_to_streams(streams_raw,
                    user_profile,
                    autocreate=False,
                    invite_only=False):
    """Converts plaintext stream names to a list of Streams, validating input in the process

    For each stream name, we validate it to ensure it meets our
    requirements for a proper stream name: that is, that it is shorter
    than Stream.MAX_NAME_LENGTH characters and passes
    valid_stream_name.

    This function in autocreate mode should be atomic: either an exception will be raised
    during a precheck, or all the streams specified will have been created if applicable.

    @param streams_raw The list of stream names to process
    @param user_profile The user for whom we are retreiving the streams
    @param autocreate Whether we should create streams if they don't already exist
    @param invite_only Whether newly created streams should have the invite_only bit set
    """
    existing_streams = []
    created_streams = []
    # Validate all streams, getting extant ones, then get-or-creating the rest.
    stream_set = set(stream_name.strip() for stream_name in streams_raw)
    rejects = []
    for stream_name in stream_set:
        if len(stream_name) > Stream.MAX_NAME_LENGTH:
            raise JsonableError("Stream name (%s) too long." % (stream_name, ))
        if not valid_stream_name(stream_name):
            raise JsonableError("Invalid stream name (%s)." % (stream_name, ))

    existing_stream_map = bulk_get_streams(user_profile.realm, stream_set)

    for stream_name in stream_set:
        stream = existing_stream_map.get(stream_name.lower())
        if stream is None:
            rejects.append(stream_name)
        else:
            existing_streams.append(stream)
    if autocreate:
        for stream_name in rejects:
            stream, created = create_stream_if_needed(user_profile.realm,
                                                      stream_name,
                                                      invite_only=invite_only)
            if created:
                created_streams.append(stream)
            else:
                existing_streams.append(stream)
    elif rejects:
        raise JsonableError("Stream(s) (%s) do not exist" % ", ".join(rejects))

    return existing_streams, created_streams
예제 #8
0
def send_response_message(bot_id: str, message_info: Dict[str, Any],
                          response_data: Dict[str, Any]) -> None:
    """
    bot_id is the user_id of the bot sending the response

    message_info is used to address the message and should have these fields:
        type - "stream" or "private"
        display_recipient - like we have in other message events
        topic - see get_topic_from_message_info

    response_data is what the bot wants to send back and has these fields:
        content - raw markdown content for Zulip to render
    """

    message_type = message_info['type']
    display_recipient = message_info['display_recipient']
    try:
        topic_name = get_topic_from_message_info(message_info)
    except KeyError:
        topic_name = None

    bot_user = get_user_profile_by_id(bot_id)
    realm = bot_user.realm
    client = get_client('OutgoingWebhookResponse')

    content = response_data.get('content')
    if not content:
        raise JsonableError(_("Missing content"))

    widget_content = response_data.get('widget_content')

    if message_type == 'stream':
        message_to = [display_recipient]
    elif message_type == 'private':
        message_to = [recipient['email'] for recipient in display_recipient]
    else:
        raise JsonableError(_("Invalid message type"))

    check_send_message(
        sender=bot_user,
        client=client,
        message_type_name=message_type,
        message_to=message_to,
        topic_name=topic_name,
        message_content=content,
        widget_content=widget_content,
        realm=realm,
    )
예제 #9
0
def is_public_stream(stream, realm):
    if not valid_stream_name(stream):
        raise JsonableError(_("Invalid stream name"))
    stream = get_stream(stream, realm)
    if stream is None:
        return False
    return stream.is_public()
예제 #10
0
    def convert_term(elem):
        # type: (Union[Dict, List]) -> Dict[str, Any]

        # We have to support a legacy tuple format.
        if isinstance(elem, list):
            if (len(elem) != 2 or
                any(not isinstance(x, str) and not isinstance(x, Text)
                    for x in elem)):
                raise ValueError("element is not a string pair")
            return dict(operator=elem[0], operand=elem[1])

        if isinstance(elem, dict):
            validator = check_dict([
                ('operator', check_string),
                ('operand', check_string),
            ])

            error = validator('elem', elem)
            if error:
                raise JsonableError(error)

            # whitelist the fields we care about for now
            return dict(
                operator=elem['operator'],
                operand=elem['operand'],
                negated=elem.get('negated', False),
            )

        raise ValueError("element is not a dictionary")
예제 #11
0
 def my_converter(data):
     lst = ujson.loads(data)
     if not isinstance(lst, list):
         raise ValueError('not a list')
     if 13 in lst:
         raise JsonableError('13 is an unlucky number!')
     return lst
예제 #12
0
def fetch_events(user_profile_id, user_profile_realm_id, user_profile_email,
                 queue_id, last_event_id, event_types, client_type_name,
                 apply_markdown, all_public_streams, lifespan_secs, narrow,
                 dont_block, handler_id):
    was_connected = False
    orig_queue_id = queue_id
    extra_log_data = ""
    if queue_id is None:
        if dont_block:
            client = allocate_client_descriptor(user_profile_id,
                                                user_profile_email,
                                                user_profile_realm_id,
                                                event_types,
                                                client_type_name,
                                                apply_markdown,
                                                all_public_streams,
                                                lifespan_secs,
                                                narrow=narrow)
            queue_id = client.event_queue.id
        else:
            raise JsonableError("Missing 'queue_id' argument")
    else:
        if last_event_id is None:
            raise JsonableError("Missing 'last_event_id' argument")
        client = get_client_descriptor(queue_id)
        if client is None:
            raise JsonableError("Bad event queue id: %s" % (queue_id, ))
        if user_profile_id != client.user_profile_id:
            raise JsonableError(
                "You are not authorized to get events from this queue")
        client.event_queue.prune(last_event_id)
        was_connected = client.finish_current_handler()

    if not client.event_queue.empty() or dont_block:
        ret = {'events': client.event_queue.contents()}
        if orig_queue_id is None:
            ret['queue_id'] = queue_id
        extra_log_data = "[%s/%s]" % (queue_id, len(ret["events"]))
        if was_connected:
            extra_log_data += " [was connected]"
        return (ret, extra_log_data)

    if was_connected:
        logging.info("Disconnected handler for queue %s (%s/%s)" %
                     (queue_id, user_profile_email, client_type_name))
    client.connect_handler(handler_id, client_type_name)
    return (RespondAsynchronously, None)
예제 #13
0
    def authenticate_client(self, msg):
        # type: (Dict[str, Any]) -> None
        if self.authenticated:
            self.session.send_message({'req_id': msg['req_id'], 'type': 'response',
                                       'response': {'result': 'error', 'msg': 'Already authenticated'}})
            return

        user_profile = get_user_profile(self.browser_session_id)
        if user_profile is None:
            raise JsonableError(_('Unknown or missing session'))
        self.session.user_profile = user_profile

        if not _compare_salted_tokens(msg['request']['csrf_token'], self.csrf_token):
            raise JsonableError(_('CSRF token does not match that in cookie'))

        if 'queue_id' not in msg['request']:
            raise JsonableError(_("Missing 'queue_id' argument"))

        queue_id = msg['request']['queue_id']
        client = get_client_descriptor(queue_id)
        if client is None:
            raise BadEventQueueIdError(queue_id)

        if user_profile.id != client.user_profile_id:
            raise JsonableError(_("You are not the owner of the queue with id '%s'") % (queue_id,))

        self.authenticated = True
        register_connection(queue_id, self)

        response = {'req_id': msg['req_id'], 'type': 'response',
                    'response': {'result': 'success', 'msg': ''}}

        status_inquiries = msg['request'].get('status_inquiries')
        if status_inquiries is not None:
            results = {}
            for inquiry in status_inquiries:
                status = redis_client.hgetall(req_redis_key(inquiry))
                if len(status) == 0:
                    status['status'] = 'not_received'
                if 'response' in status:
                    status['response'] = ujson.loads(status['response'])
                results[str(inquiry)] = status
            response['response']['status_inquiries'] = results

        self.session.send_message(response)
        ioloop = tornado.ioloop.IOLoop.instance()
        ioloop.remove_timeout(self.timeout_handle)
예제 #14
0
파일: streams.py 프로젝트: kevinwmx/zulip
def get_subscribers_backend(request, user_profile, stream_name=REQ('stream')):
    stream = get_stream(stream_name, user_profile.realm)
    if stream is None:
        raise JsonableError("Stream does not exist: %s" % (stream_name, ))

    subscribers = get_subscriber_emails(stream, user_profile)

    return json_success({'subscribers': subscribers})
예제 #15
0
def stream_or_none(stream_name, realm):
    if stream_name == '':
        return None
    else:
        stream = get_stream(stream_name, realm)
        if not stream:
            raise JsonableError('No such stream \'%s\'' % (stream_name, ))
        return stream
예제 #16
0
파일: users.py 프로젝트: shekhirin/zulip
def stream_or_none(stream_name, realm):
    # type: (text_type, Realm) -> Optional[Stream]
    if stream_name == '':
        return None
    else:
        stream = get_stream(stream_name, realm)
        if not stream:
            raise JsonableError(_('No such stream \'%s\'') % (stream_name, ))
        return stream
예제 #17
0
def do_rest_call(rest_operation: Dict[str, Any],
                 request_data: Optional[Dict[str, Any]],
                 event: Dict[str, Any],
                 service_handler: Any,
                 timeout: Any=None) -> None:
    rest_operation_validator = check_dict([
        ('method', check_string),
        ('relative_url_path', check_string),
        ('request_kwargs', check_dict([])),
        ('base_url', check_string),
    ])

    error = rest_operation_validator('rest_operation', rest_operation)
    if error:
        raise JsonableError(error)

    http_method = rest_operation['method']
    final_url = urllib.parse.urljoin(rest_operation['base_url'], rest_operation['relative_url_path'])
    request_kwargs = rest_operation['request_kwargs']
    request_kwargs['timeout'] = timeout

    try:
        response = requests.request(http_method, final_url, data=request_data, **request_kwargs)
        if str(response.status_code).startswith('2'):
            process_success_response(event, service_handler, response)
        else:
            logging.warning("Message %(message_url)s triggered an outgoing webhook, returning status "
                            "code %(status_code)s.\n Content of response (in quotes): \""
                            "%(response)s\""
                            % {'message_url': get_message_url(event, request_data),
                               'status_code': response.status_code,
                               'response': response.content})
            failure_message = "Third party responded with %d" % (response.status_code)
            fail_with_message(event, failure_message)
            notify_bot_owner(event, request_data, response.status_code, response.content)

    except requests.exceptions.Timeout as e:
        logging.info("Trigger event %s on %s timed out. Retrying" % (
            event["command"], event['service_name']))
        request_retry(event, request_data, 'Unable to connect with the third party.', exception=e)

    except requests.exceptions.ConnectionError as e:
        response_message = ("The message `%s` resulted in a connection error when "
                            "sending a request to an outgoing "
                            "webhook! See the Zulip server logs for more information." % (event["command"],))
        logging.info("Trigger event %s on %s resulted in a connection error. Retrying"
                     % (event["command"], event['service_name']))
        request_retry(event, request_data, response_message, exception=e)

    except requests.exceptions.RequestException as e:
        response_message = ("An exception of type *%s* occurred for message `%s`! "
                            "See the Zulip server logs for more information." % (
                                type(e).__name__, event["command"],))
        logging.exception("Outhook trigger failed:\n %s" % (e,))
        fail_with_message(event, response_message)
        notify_bot_owner(event, request_data, exception=e)
예제 #18
0
파일: upload.py 프로젝트: anujsrc/zulip
def claim_attachment(path_id, message):
    try:
        attachment = Attachment.objects.get(path_id=path_id)
        attachment.messages.add(message)
        attachment.save()
        return True
    except Attachment.DoesNotExist:
        raise JsonableError(
            "The upload was not successful. Please reupload the file again in a new message."
        )
    return False
예제 #19
0
def send_response_message(
    bot_id: int, message_info: Dict[str, Any], response_data: Dict[str, Any]
) -> None:
    """
    bot_id is the user_id of the bot sending the response

    message_info is used to address the message and should have these fields:
        type - "stream" or "private"
        display_recipient - like we have in other message events
        topic - see get_topic_from_message_info

    response_data is what the bot wants to send back and has these fields:
        content - raw Markdown content for Zulip to render

    WARNING: This function sends messages bypassing the stream access check
    for the bot - so use with caution to not call this in codepaths
    that might let someone send arbitrary messages to any stream through this.
    """

    message_type = message_info["type"]
    display_recipient = message_info["display_recipient"]
    try:
        topic_name: Optional[str] = get_topic_from_message_info(message_info)
    except KeyError:
        topic_name = None

    bot_user = get_user_profile_by_id(bot_id)
    realm = bot_user.realm
    client = get_client("OutgoingWebhookResponse")

    content = response_data.get("content")
    assert content

    widget_content = response_data.get("widget_content")

    if message_type == "stream":
        message_to = [display_recipient]
    elif message_type == "private":
        message_to = [recipient["email"] for recipient in display_recipient]
    else:
        raise JsonableError(_("Invalid message type"))

    check_send_message(
        sender=bot_user,
        client=client,
        message_type_name=message_type,
        message_to=message_to,
        topic_name=topic_name,
        message_content=content,
        widget_content=widget_content,
        realm=realm,
        skip_stream_access_check=True,
    )
예제 #20
0
파일: messages.py 프로젝트: tied/zulip
def update_message_flags(request,
                         user_profile,
                         messages=REQ(validator=check_list(check_int)),
                         operation=REQ('op'),
                         flag=REQ(),
                         all=REQ(validator=check_bool, default=False),
                         stream_name=REQ(default=None),
                         topic_name=REQ(default=None)):
    # type: (HttpRequest, UserProfile, List[int], text_type, text_type, bool, Optional[text_type], Optional[text_type]) -> HttpResponse
    if all:
        target_count_str = "all"
    else:
        target_count_str = str(len(messages))
    log_data_str = "[%s %s/%s]" % (operation, flag, target_count_str)
    request._log_data["extra"] = log_data_str
    stream = None
    if stream_name is not None:
        stream = get_stream(stream_name, user_profile.realm)
        if not stream:
            raise JsonableError(_('No such stream \'%s\'') % (stream_name, ))
        if topic_name:
            topic_exists = UserMessage.objects.filter(
                user_profile=user_profile,
                message__recipient__type_id=stream.id,
                message__recipient__type=Recipient.STREAM,
                message__subject__iexact=topic_name).exists()
            if not topic_exists:
                raise JsonableError(_('No such topic \'%s\'') % (topic_name, ))
    count = do_update_message_flags(user_profile, operation, flag, messages,
                                    all, stream, topic_name)

    # If we succeed, update log data str with the actual count for how
    # many messages were updated.
    if count != len(messages):
        log_data_str = "[%s %s/%s] actually %s" % (operation, flag,
                                                   target_count_str, count)
    request._log_data["extra"] = log_data_str

    return json_success({'result': 'success', 'messages': messages, 'msg': ''})
예제 #21
0
def do_rest_call(rest_operation,
                 request_data,
                 event,
                 service_handler,
                 timeout=None):
    # type: (Dict[str, Any], Optional[Dict[str, Any]], Dict[str, Any], Any, Any) -> None
    rest_operation_validator = check_dict([
        ('method', check_string),
        ('relative_url_path', check_string),
        ('request_kwargs', check_dict([])),
        ('base_url', check_string),
    ])

    error = rest_operation_validator('rest_operation', rest_operation)
    if error:
        raise JsonableError(error)

    http_method = rest_operation['method']
    final_url = urllib.parse.urljoin(rest_operation['base_url'],
                                     rest_operation['relative_url_path'])
    request_kwargs = rest_operation['request_kwargs']
    request_kwargs['timeout'] = timeout

    try:
        response = requests.request(http_method,
                                    final_url,
                                    data=request_data,
                                    **request_kwargs)
        if str(response.status_code).startswith('2'):
            response_message = service_handler.process_success(response, event)
            if response_message is not None:
                succeed_with_message(event, response_message)

        # On 50x errors, try retry
        elif str(response.status_code).startswith('5'):
            request_retry(event, "Internal Server error at third party.")
        else:
            failure_message = "Third party responded with %d" % (
                response.status_code)
            fail_with_message(event, failure_message)

    except requests.exceptions.Timeout:
        logging.info("Trigger event %s on %s timed out. Retrying" %
                     (event["command"], event['service_name']))
        request_retry(event, 'Unable to connect with the third party.')

    except requests.exceptions.RequestException as e:
        response_message = "An exception occured for message `%s`! See the logs for more information." % (
            event["command"], )
        logging.exception("Outhook trigger failed:\n %s" % (e, ))
        fail_with_message(event, response_message)
예제 #22
0
def send_response_message(bot_id: str, message: Dict[str, Any], response_message_content: Text) -> None:
    recipient_type_name = message['type']
    bot_user = get_user_profile_by_id(bot_id)
    realm = bot_user.realm

    if recipient_type_name == 'stream':
        recipients = [message['display_recipient']]
        check_send_message(bot_user, get_client("OutgoingWebhookResponse"), recipient_type_name, recipients,
                           message['subject'], response_message_content, realm)
    elif recipient_type_name == 'private':
        recipients = [recipient['email'] for recipient in message['display_recipient']]
        check_send_message(bot_user, get_client("OutgoingWebhookResponse"), recipient_type_name, recipients,
                           None, response_message_content, realm)
    else:
        raise JsonableError(_("Invalid message type"))
예제 #23
0
def update_message_backend(request, user_profile,
                           message_id=REQ(converter=to_non_negative_int),
                           subject=REQ(default=None),
                           propagate_mode=REQ(default="change_one"),
                           content=REQ(default=None)):
    # type: (HttpRequest, UserProfile, int, Optional[text_type], Optional[str], Optional[text_type]) -> HttpResponse
    if not user_profile.realm.allow_message_editing:
        return json_error(_("Your organization has turned off message editing."))

    try:
        message = Message.objects.select_related().get(id=message_id)
    except Message.DoesNotExist:
        raise JsonableError(_("Unknown message id"))

    # You only have permission to edit a message if:
    # 1. You sent it, OR:
    # 2. This is a topic-only edit for a (no topic) message, OR:
    # 3. This is a topic-only edit and you are an admin.
    if message.sender == user_profile:
        pass
    elif (content is None) and ((message.topic_name() == "(no topic)") or
                                user_profile.is_realm_admin):
        pass
    else:
        raise JsonableError(_("You don't have permission to edit this message"))

    # If there is a change to the content, check that it hasn't been too long
    # Allow an extra 20 seconds since we potentially allow editing 15 seconds
    # past the limit, and in case there are network issues, etc. The 15 comes
    # from (min_seconds_to_edit + seconds_left_buffer) in message_edit.js; if
    # you change this value also change those two parameters in message_edit.js.
    edit_limit_buffer = 20
    if content is not None and user_profile.realm.message_content_edit_limit_seconds > 0:
        deadline_seconds = user_profile.realm.message_content_edit_limit_seconds + edit_limit_buffer
        if (now() - message.pub_date) > datetime.timedelta(seconds=deadline_seconds):
            raise JsonableError(_("The time limit for editing this message has past"))

    if subject is None and content is None:
        return json_error(_("Nothing to change"))
    if subject is not None:
        subject = subject.strip()
        if subject == "":
            raise JsonableError(_("Topic can't be empty"))
    rendered_content = None
    if content is not None:
        content = content.strip()
        if content == "":
            raise JsonableError(_("Content can't be empty"))
        content = truncate_body(content)
        rendered_content = message.render_markdown(content)
        if not rendered_content:
            raise JsonableError(_("We were unable to render your updated message"))

    do_update_message(user_profile, message, subject, propagate_mode, content, rendered_content)
    return json_success()
예제 #24
0
def do_rest_call(rest_operation, event, timeout=None):
    # type: (Dict[str, Any], Dict[str, Any], Any) -> None
    rest_operation_validator = check_dict([
        ('method', check_string),
        ('relative_url_path', check_string),
        ('request_kwargs', check_dict([])),
        ('base_url', check_string),
    ])

    error = rest_operation_validator('rest_operation', rest_operation)
    if error:
        raise JsonableError(error)

    http_method = rest_operation['method']
    final_url = urllib.parse.urljoin(rest_operation['base_url'],
                                     rest_operation['relative_url_path'])
    request_kwargs = rest_operation['request_kwargs']
    request_kwargs['timeout'] = timeout

    try:
        # TODO: Add comment describing structure of data being sent to third party URL.
        response = requests.request(http_method,
                                    final_url,
                                    data=json.dumps(event),
                                    **request_kwargs)
        if str(response.status_code).startswith('2'):
            succeed_with_message(
                event, "received response: `" + str(response.content) + "`.")

        # On 50x errors, try retry
        elif str(response.status_code).startswith('5'):
            request_retry(event, "unable to connect with the third party.")
        else:
            fail_with_message(event,
                              "unable to communicate with the third party.")

    except requests.exceptions.Timeout:
        logging.info("Trigger event %s on %s timed out. Retrying" %
                     (event["command"], event['service_name']))
        request_retry(event, 'unable to connect with the third party.')

    except requests.exceptions.RequestException as e:
        response_message = "An exception occured for message `%s`! See the logs for more information." % (
            event["command"], )
        logging.exception("Outhook trigger failed:\n %s" % (e, ))
        fail_with_message(event, response_message)
예제 #25
0
def check_supported_events_narrow_filter(narrow):
    for element in narrow:
        operator = element[0]
        if operator not in ["stream", "topic", "sender", "is"]:
            raise JsonableError("Operator %s not supported." % (operator,))
예제 #26
0
def update_message_backend(request, user_profile,
                           message_id=REQ(converter=to_non_negative_int),
                           subject=REQ(default=None),
                           propagate_mode=REQ(default="change_one"),
                           content=REQ(default=None)):
    # type: (HttpRequest, UserProfile, int, Optional[Text], Optional[str], Optional[Text]) -> HttpResponse
    if not user_profile.realm.allow_message_editing:
        return json_error(_("Your organization has turned off message editing."))

    message, ignored_user_message = access_message(user_profile, message_id)

    # You only have permission to edit a message if:
    # 1. You sent it, OR:
    # 2. This is a topic-only edit for a (no topic) message, OR:
    # 3. This is a topic-only edit and you are an admin.
    if message.sender == user_profile:
        pass
    elif (content is None) and ((message.topic_name() == "(no topic)") or
                                user_profile.is_realm_admin):
        pass
    else:
        raise JsonableError(_("You don't have permission to edit this message"))

    # If there is a change to the content, check that it hasn't been too long
    # Allow an extra 20 seconds since we potentially allow editing 15 seconds
    # past the limit, and in case there are network issues, etc. The 15 comes
    # from (min_seconds_to_edit + seconds_left_buffer) in message_edit.js; if
    # you change this value also change those two parameters in message_edit.js.
    edit_limit_buffer = 20
    if content is not None and user_profile.realm.message_content_edit_limit_seconds > 0:
        deadline_seconds = user_profile.realm.message_content_edit_limit_seconds + edit_limit_buffer
        if (timezone.now() - message.pub_date) > datetime.timedelta(seconds=deadline_seconds):
            raise JsonableError(_("The time limit for editing this message has past"))

    if subject is None and content is None:
        return json_error(_("Nothing to change"))
    if subject is not None:
        subject = subject.strip()
        if subject == "":
            raise JsonableError(_("Topic can't be empty"))
    rendered_content = None
    links_for_embed = set()  # type: Set[Text]
    if content is not None:
        content = content.strip()
        if content == "":
            content = "(deleted)"
        content = truncate_body(content)

        # We exclude UserMessage.flags.historical rows since those
        # users did not receive the message originally, and thus
        # probably are not relevant for reprocessed alert_words,
        # mentions and similar rendering features.  This may be a
        # decision we change in the future.
        ums = UserMessage.objects.filter(
            message=message.id,
            flags=~UserMessage.flags.historical)

        message_users = UserProfile.objects.select_related().filter(
            id__in={um.user_profile_id for um in ums})

        # We render the message using the current user's realm; since
        # the cross-realm bots never edit messages, this should be
        # always correct.
        # Note: If rendering fails, the called code will raise a JsonableError.
        rendered_content = render_incoming_message(message,
                                                   content,
                                                   message_users,
                                                   user_profile.realm)
        links_for_embed |= message.links_for_preview

    number_changed = do_update_message(user_profile, message, subject,
                                       propagate_mode, content, rendered_content)
    # Include the number of messages changed in the logs
    request._log_data['extra'] = "[%s]" % (number_changed,)
    if links_for_embed and getattr(settings, 'INLINE_URL_EMBED_PREVIEW', None):
        event_data = {
            'message_id': message.id,
            'message_content': message.content,
            # The choice of `user_profile.realm_id` rather than
            # `sender.realm_id` must match the decision made in the
            # `render_incoming_message` call earlier in this function.
            'message_realm_id': user_profile.realm_id,
            'urls': links_for_embed}
        queue_json_publish('embed_links', event_data, lambda x: None)
    return json_success()
예제 #27
0
def do_rest_call(rest_operation,
                 request_data,
                 event,
                 service_handler,
                 timeout=None):
    # type: (Dict[str, Any], Optional[Dict[str, Any]], Dict[str, Any], Any, Any) -> None
    rest_operation_validator = check_dict([
        ('method', check_string),
        ('relative_url_path', check_string),
        ('request_kwargs', check_dict([])),
        ('base_url', check_string),
    ])

    error = rest_operation_validator('rest_operation', rest_operation)
    if error:
        raise JsonableError(error)

    bot_user = get_user_profile_by_id(event['user_profile_id'])

    http_method = rest_operation['method']
    final_url = urllib.parse.urljoin(rest_operation['base_url'],
                                     rest_operation['relative_url_path'])
    request_kwargs = rest_operation['request_kwargs']
    request_kwargs['timeout'] = timeout

    try:
        response = requests.request(http_method,
                                    final_url,
                                    data=request_data,
                                    **request_kwargs)
        if str(response.status_code).startswith('2'):
            response_message = service_handler.process_success(response, event)
            if response_message is not None:
                succeed_with_message(event, response_message)
        else:
            message_url = (
                "%(server)s/#narrow/stream/%(stream)s/subject/%(subject)s/near/%(id)s"
                % {
                    'server': bot_user.realm.uri,
                    'stream': event['message']['display_recipient'],
                    'subject': event['message']['subject'],
                    'id': str(event['message']['id'])
                })
            logging.warning(
                "Message %(message_url)s triggered an outgoing webhook, returning status "
                "code %(status_code)s.\n Content of response (in quotes): \""
                "%(response)s\"" % {
                    'message_url': message_url,
                    'status_code': response.status_code,
                    'response': response.content
                })
            # On 50x errors, try retry
            if str(response.status_code).startswith('5'):
                request_retry(event, "Internal Server error at third party.")
            else:
                failure_message = "Third party responded with %d" % (
                    response.status_code)
                fail_with_message(event, failure_message)

    except requests.exceptions.Timeout:
        logging.info("Trigger event %s on %s timed out. Retrying" %
                     (event["command"], event['service_name']))
        request_retry(event, 'Unable to connect with the third party.')

    except requests.exceptions.RequestException as e:
        response_message = "An exception occured for message `%s`! See the logs for more information." % (
            event["command"], )
        logging.exception("Outhook trigger failed:\n %s" % (e, ))
        fail_with_message(event, response_message)
예제 #28
0
def update_message_backend(request,
                           user_profile,
                           message_id=REQ(converter=to_non_negative_int),
                           subject=REQ(default=None),
                           propagate_mode=REQ(default="change_one"),
                           content=REQ(default=None)):
    # type: (HttpRequest, UserProfile, int, Optional[text_type], Optional[str], Optional[text_type]) -> HttpResponse
    if not user_profile.realm.allow_message_editing:
        return json_error(
            _("Your organization has turned off message editing."))

    try:
        message = Message.objects.select_related().get(id=message_id)
    except Message.DoesNotExist:
        raise JsonableError(_("Unknown message id"))

    # You only have permission to edit a message if:
    # 1. You sent it, OR:
    # 2. This is a topic-only edit for a (no topic) message, OR:
    # 3. This is a topic-only edit and you are an admin.
    if message.sender == user_profile:
        pass
    elif (content is None) and ((message.topic_name() == "(no topic)")
                                or user_profile.is_realm_admin):
        pass
    else:
        raise JsonableError(
            _("You don't have permission to edit this message"))

    # If there is a change to the content, check that it hasn't been too long
    # Allow an extra 20 seconds since we potentially allow editing 15 seconds
    # past the limit, and in case there are network issues, etc. The 15 comes
    # from (min_seconds_to_edit + seconds_left_buffer) in message_edit.js; if
    # you change this value also change those two parameters in message_edit.js.
    edit_limit_buffer = 20
    if content is not None and user_profile.realm.message_content_edit_limit_seconds > 0:
        deadline_seconds = user_profile.realm.message_content_edit_limit_seconds + edit_limit_buffer
        if (now() - message.pub_date) > datetime.timedelta(
                seconds=deadline_seconds):
            raise JsonableError(
                _("The time limit for editing this message has past"))

    if subject is None and content is None:
        return json_error(_("Nothing to change"))
    if subject is not None:
        subject = subject.strip()
        if subject == "":
            raise JsonableError(_("Topic can't be empty"))
    rendered_content = None
    if content is not None:
        content = content.strip()
        if content == "":
            content = "(deleted)"
        content = truncate_body(content)

        # We exclude UserMessage.flags.historical rows since those
        # users did not receive the message originally, and thus
        # probably are not relevant for reprocessed alert_words,
        # mentions and similar rendering features.  This may be a
        # decision we change in the future.
        ums = UserMessage.objects.filter(message=message.id,
                                         flags=~UserMessage.flags.historical)
        message_users = {
            get_user_profile_by_id(um.user_profile_id)
            for um in ums
        }
        # If rendering fails, the called code will raise a JsonableError.
        rendered_content = render_incoming_message(message,
                                                   content=content,
                                                   message_users=message_users)

    do_update_message(user_profile, message, subject, propagate_mode, content,
                      rendered_content)
    return json_success()