def send_zulip(sender: UserProfile, stream: Stream, topic: str, content: str) -> None: internal_send_stream_message(stream.realm, sender, stream, truncate_topic(topic), truncate_body(content), email_gateway=True)
def further_validated_draft_dict(draft_dict: Dict[str, Any], user_profile: UserProfile) -> Dict[str, Any]: """ Take a draft_dict that was already validated by draft_dict_validator then further sanitize, validate, and transform it. Ultimately return this "further validated" draft dict. It will have a slightly different set of keys the values for which can be used to directly create a Draft object. """ content = truncate_body(draft_dict["content"]) if "\x00" in content: raise JsonableError(_("Content must not contain null bytes")) timestamp = draft_dict.get("timestamp", time.time()) timestamp = round(timestamp, 6) if timestamp < 0: # While it's not exactly an invalid timestamp, it's not something # we want to allow either. raise JsonableError(_("Timestamp must not be negative.")) last_edit_time = timestamp_to_datetime(timestamp) topic = "" recipient = None to = draft_dict["to"] if draft_dict["type"] == "stream": topic = truncate_topic(draft_dict["topic"]) if "\x00" in topic: raise JsonableError(_("Topic must not contain null bytes")) if len(to) != 1: raise JsonableError( _("Must specify exactly 1 stream ID for stream messages")) stream, sub = access_stream_by_id(user_profile, to[0]) recipient = stream.recipient elif draft_dict["type"] == "private" and len(to) != 0: to_users = get_user_profiles_by_ids(set(to), user_profile.realm) try: recipient = recipient_for_user_profiles(to_users, False, None, user_profile) except ValidationError as e: # nocoverage raise JsonableError(e.messages[0]) return { "recipient": recipient, "topic": topic, "content": content, "last_edit_time": last_edit_time, }
def update_message_backend( request: HttpRequest, user_profile: UserMessage, message_id: int = REQ(converter=to_non_negative_int, path_only=True), stream_id: Optional[int] = REQ(converter=to_non_negative_int, default=None), topic_name: Optional[str] = REQ_topic(), propagate_mode: Optional[str] = REQ( default="change_one", str_validator=check_string_in(PROPAGATE_MODE_VALUES)), send_notification_to_old_thread: bool = REQ(default=True, validator=check_bool), send_notification_to_new_thread: bool = REQ(default=True, validator=check_bool), content: Optional[str] = REQ(default=None) ) -> HttpResponse: if not user_profile.realm.allow_message_editing: return json_error( _("Your organization has turned off message editing")) if propagate_mode != "change_one" and topic_name is None and stream_id is None: return json_error(_("Invalid propagate_mode without topic edit")) message, ignored_user_message = access_message(user_profile, message_id) is_no_topic_msg = (message.topic_name() == "(no topic)") # You only have permission to edit a message if: # you change this value also change those two parameters in message_edit.js. # 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, OR: # 4. This is a topic-only edit and your realm allows users to edit topics. if message.sender == user_profile: pass elif (content is None) and (is_no_topic_msg or user_profile.is_realm_admin or user_profile.realm.allow_community_topic_editing): 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.date_sent) > datetime.timedelta( seconds=deadline_seconds): raise JsonableError( _("The time limit for editing this message has passed")) # If there is a change to the topic, check that the user is allowed to # edit it and that it has not been too long. If this is not the user who # sent the message, they are not the admin, and the time limit for editing # topics is passed, raise an error. if content is None and message.sender != user_profile and not user_profile.is_realm_admin and \ not is_no_topic_msg: deadline_seconds = Realm.DEFAULT_COMMUNITY_TOPIC_EDITING_LIMIT_SECONDS + edit_limit_buffer if (timezone_now() - message.date_sent) > datetime.timedelta( seconds=deadline_seconds): raise JsonableError( _("The time limit for editing this message has passed")) if topic_name is None and content is None and stream_id is None: return json_error(_("Nothing to change")) if topic_name is not None: topic_name = topic_name.strip() if topic_name == "": raise JsonableError(_("Topic can't be empty")) rendered_content = None links_for_embed: Set[str] = set() prior_mention_user_ids: Set[int] = set() mention_user_ids: Set[int] = set() mention_data: Optional[bugdown.MentionData] = None if content is not None: content = content.strip() if content == "": content = "(deleted)" content = truncate_body(content) mention_data = bugdown.MentionData( realm_id=user_profile.realm.id, content=content, ) user_info = get_user_info_for_message_updates(message.id) prior_mention_user_ids = user_info['mention_user_ids'] # 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, user_info['message_user_ids'], user_profile.realm, mention_data=mention_data) links_for_embed |= message.links_for_preview mention_user_ids = message.mentions_user_ids new_stream = None old_stream = None number_changed = 0 if stream_id is not None: if not user_profile.is_realm_admin: raise JsonableError( _("You don't have permission to move this message")) if content is not None: raise JsonableError( _("Cannot change message content while changing stream")) old_stream = get_stream_by_id(message.recipient.type_id) new_stream = get_stream_by_id(stream_id) if not (old_stream.is_public() and new_stream.is_public()): # We'll likely decide to relax this condition in the # future; it just requires more care with details like the # breadcrumb messages. raise JsonableError(_("Streams must be public")) number_changed = do_update_message( user_profile, message, new_stream, topic_name, propagate_mode, send_notification_to_old_thread, send_notification_to_new_thread, content, rendered_content, prior_mention_user_ids, mention_user_ids, mention_data) # Include the number of messages changed in the logs request._log_data['extra'] = f"[{number_changed}]" if links_for_embed: 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) return json_success()