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)
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)
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': ''})
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")
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)
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
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
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, )
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()
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")
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
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)
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)
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})
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
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
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)
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
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, )
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': ''})
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)
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"))
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()
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)
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,))
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()
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)
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()