views: Extract message_edit.py for message editing views.

This is a pretty clean extraction of files that lets us shrink one of
our largest files.
This commit is contained in:
Tim Abbott 2020-06-22 13:57:01 -07:00 committed by Tim Abbott
parent 1a6799f15e
commit 4d7550d705
7 changed files with 286 additions and 274 deletions

View File

@ -254,7 +254,7 @@ function edit_message(row, raw_content) {
// been able to click it at the time the mouse entered the message_row. Also // been able to click it at the time the mouse entered the message_row. Also
// a buffer in case their computer is slow, or stalled for a second, etc // a buffer in case their computer is slow, or stalled for a second, etc
// If you change this number also change edit_limit_buffer in // If you change this number also change edit_limit_buffer in
// zerver.views.messages.update_message_backend // zerver.views.message_edit.update_message_backend
const seconds_left_buffer = 5; const seconds_left_buffer = 5;
const editability = get_editability(message, seconds_left_buffer); const editability = get_editability(message, seconds_left_buffer);
const is_editable = editability === exports.editability_types.TOPIC_ONLY || const is_editable = editability === exports.editability_types.TOPIC_ONLY ||
@ -366,7 +366,7 @@ function edit_message(row, raw_content) {
page_params.realm_message_content_edit_limit_seconds > 0) { page_params.realm_message_content_edit_limit_seconds > 0) {
// Give them at least 10 seconds. // Give them at least 10 seconds.
// If you change this number also change edit_limit_buffer in // If you change this number also change edit_limit_buffer in
// zerver.views.messages.update_message_backend // zerver.views.message_edit.update_message_backend
const min_seconds_to_edit = 10; const min_seconds_to_edit = 10;
const now = new XDate(); const now = new XDate();
let seconds_left = page_params.realm_message_content_edit_limit_seconds + let seconds_left = page_params.realm_message_content_edit_limit_seconds +

View File

@ -283,7 +283,7 @@ class PreviewTestCase(ZulipTestCase):
url = 'http://test.org/' url = 'http://test.org/'
mocked_response = mock.Mock(side_effect=self.create_mock_response(url)) mocked_response = mock.Mock(side_effect=self.create_mock_response(url))
with mock.patch('zerver.views.messages.queue_json_publish') as patched: with mock.patch('zerver.views.message_edit.queue_json_publish') as patched:
result = self.client_patch("/json/messages/" + str(msg_id), { result = self.client_patch("/json/messages/" + str(msg_id), {
'message_id': msg_id, 'content': url, 'message_id': msg_id, 'content': url,
}) })
@ -378,7 +378,7 @@ class PreviewTestCase(ZulipTestCase):
self.assertIn(f'<a href="{edited_url}" title="The Rock">The Rock</a>', self.assertIn(f'<a href="{edited_url}" title="The Rock">The Rock</a>',
msg.rendered_content) msg.rendered_content)
with mock.patch('zerver.views.messages.queue_json_publish', wraps=wrapped_queue_json_publish) as patched: with mock.patch('zerver.views.message_edit.queue_json_publish', wraps=wrapped_queue_json_publish) as patched:
result = self.client_patch("/json/messages/" + str(msg_id), { result = self.client_patch("/json/messages/" + str(msg_id), {
'message_id': msg_id, 'content': edited_url, 'message_id': msg_id, 'content': edited_url,
}) })

View File

@ -4629,9 +4629,9 @@ class DeleteMessageTest(ZulipTestCase):
# Test handling of 500 error caused by multiple delete requests due to latency. # Test handling of 500 error caused by multiple delete requests due to latency.
# see issue #11219. # see issue #11219.
with mock.patch("zerver.views.messages.do_delete_messages") as m, \ with mock.patch("zerver.views.message_edit.do_delete_messages") as m, \
mock.patch("zerver.views.messages.validate_can_delete_message", return_value=None), \ mock.patch("zerver.views.message_edit.validate_can_delete_message", return_value=None), \
mock.patch("zerver.views.messages.access_message", return_value=(None, None)): mock.patch("zerver.views.message_edit.access_message", return_value=(None, None)):
m.side_effect = IntegrityError() m.side_effect = IntegrityError()
result = test_delete_message_by_owner(msg_id=msg_id) result = test_delete_message_by_owner(msg_id=msg_id)
self.assert_json_error(result, "Message already deleted") self.assert_json_error(result, "Message already deleted")

View File

@ -0,0 +1,268 @@
import datetime
from typing import Any, Dict, List, Optional, Set
import ujson
from django.db import IntegrityError
from django.http import HttpRequest, HttpResponse
from django.utils.timezone import now as timezone_now
from django.utils.translation import ugettext as _
from zerver.decorator import REQ, has_request_variables
from zerver.lib import bugdown
from zerver.lib.actions import (
do_delete_messages,
do_update_message,
get_user_info_for_message_updates,
render_incoming_message,
)
from zerver.lib.exceptions import JsonableError
from zerver.lib.html_diff import highlight_html_differences
from zerver.lib.message import access_message, truncate_body
from zerver.lib.queue import queue_json_publish
from zerver.lib.response import json_error, json_success
from zerver.lib.streams import get_stream_by_id
from zerver.lib.timestamp import datetime_to_timestamp
from zerver.lib.topic import LEGACY_PREV_TOPIC, REQ_topic
from zerver.lib.validator import check_bool, check_string_in, to_non_negative_int
from zerver.models import Message, Realm, UserMessage, UserProfile
def fill_edit_history_entries(message_history: List[Dict[str, Any]], message: Message) -> None:
"""This fills out the message edit history entries from the database,
which are designed to have the minimum data possible, to instead
have the current topic + content as of that time, plus data on
whatever changed. This makes it much simpler to do future
processing.
Note that this mutates what is passed to it, which is sorta a bad pattern.
"""
prev_content = message.content
prev_rendered_content = message.rendered_content
prev_topic = message.topic_name()
# Make sure that the latest entry in the history corresponds to the
# message's last edit time
if len(message_history) > 0:
assert message.last_edit_time is not None
assert(datetime_to_timestamp(message.last_edit_time) ==
message_history[0]['timestamp'])
for entry in message_history:
entry['topic'] = prev_topic
if LEGACY_PREV_TOPIC in entry:
prev_topic = entry[LEGACY_PREV_TOPIC]
entry['prev_topic'] = prev_topic
del entry[LEGACY_PREV_TOPIC]
entry['content'] = prev_content
entry['rendered_content'] = prev_rendered_content
if 'prev_content' in entry:
del entry['prev_rendered_content_version']
prev_content = entry['prev_content']
prev_rendered_content = entry['prev_rendered_content']
assert prev_rendered_content is not None
entry['content_html_diff'] = highlight_html_differences(
prev_rendered_content,
entry['rendered_content'],
message.id)
message_history.append(dict(
topic = prev_topic,
content = prev_content,
rendered_content = prev_rendered_content,
timestamp = datetime_to_timestamp(message.date_sent),
user_id = message.sender_id,
))
@has_request_variables
def get_message_edit_history(request: HttpRequest, user_profile: UserProfile,
message_id: int=REQ(converter=to_non_negative_int,
path_only=True)) -> HttpResponse:
if not user_profile.realm.allow_edit_history:
return json_error(_("Message edit history is disabled in this organization"))
message, ignored_user_message = access_message(user_profile, message_id)
# Extract the message edit history from the message
if message.edit_history is not None:
message_edit_history = ujson.loads(message.edit_history)
else:
message_edit_history = []
# Fill in all the extra data that will make it usable
fill_edit_history_entries(message_edit_history, message)
return json_success({"message_history": reversed(message_edit_history)})
PROPAGATE_MODE_VALUES = ["change_later", "change_one", "change_all"]
@has_request_variables
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()
def validate_can_delete_message(user_profile: UserProfile, message: Message) -> None:
if user_profile.is_realm_admin:
# Admin can delete any message, any time.
return
if message.sender != user_profile:
# Users can only delete messages sent by them.
raise JsonableError(_("You don't have permission to delete this message"))
if not user_profile.realm.allow_message_deleting:
# User can not delete message, if message deleting is not allowed in realm.
raise JsonableError(_("You don't have permission to delete this message"))
deadline_seconds = user_profile.realm.message_content_delete_limit_seconds
if deadline_seconds == 0:
# 0 for no time limit to delete message
return
if (timezone_now() - message.date_sent) > datetime.timedelta(seconds=deadline_seconds):
# User can not delete message after deadline time of realm
raise JsonableError(_("The time limit for deleting this message has passed"))
return
@has_request_variables
def delete_message_backend(request: HttpRequest, user_profile: UserProfile,
message_id: int=REQ(converter=to_non_negative_int,
path_only=True)) -> HttpResponse:
message, ignored_user_message = access_message(user_profile, message_id)
validate_can_delete_message(user_profile, message)
try:
do_delete_messages(user_profile.realm, [message])
except (Message.DoesNotExist, IntegrityError):
raise JsonableError(_("Message already deleted"))
return json_success()
@has_request_variables
def json_fetch_raw_message(request: HttpRequest, user_profile: UserProfile,
message_id: int=REQ(converter=to_non_negative_int,
path_only=True)) -> HttpResponse:
(message, user_message) = access_message(user_profile, message_id)
return json_success({"raw_content": message.content})

View File

@ -1,13 +1,12 @@
import datetime
import re import re
from typing import Any, Dict, Iterable, List, Optional, Sequence, Set, Tuple, Union, cast from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union, cast
import ujson import ujson
from dateutil.parser import parse as dateparser from dateutil.parser import parse as dateparser
from django.conf import settings from django.conf import settings
from django.core import validators from django.core import validators
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import IntegrityError, connection from django.db import connection
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.utils.html import escape as escape_html from django.utils.html import escape as escape_html
from django.utils.timezone import now as timezone_now from django.utils.timezone import now as timezone_now
@ -31,35 +30,22 @@ from sqlalchemy.sql import (
) )
from zerver.decorator import REQ, has_request_variables from zerver.decorator import REQ, has_request_variables
from zerver.lib import bugdown
from zerver.lib.actions import ( from zerver.lib.actions import (
check_schedule_message, check_schedule_message,
check_send_message, check_send_message,
compute_irc_user_fullname, compute_irc_user_fullname,
compute_jabber_user_fullname, compute_jabber_user_fullname,
create_mirror_user_if_needed, create_mirror_user_if_needed,
do_delete_messages,
do_mark_all_as_read, do_mark_all_as_read,
do_mark_stream_messages_as_read, do_mark_stream_messages_as_read,
do_update_message,
do_update_message_flags, do_update_message_flags,
extract_private_recipients, extract_private_recipients,
extract_stream_indicator, extract_stream_indicator,
get_user_info_for_message_updates,
recipient_for_user_profiles, recipient_for_user_profiles,
render_incoming_message,
) )
from zerver.lib.addressee import get_user_profiles, get_user_profiles_by_ids from zerver.lib.addressee import get_user_profiles, get_user_profiles_by_ids
from zerver.lib.exceptions import ErrorCode, JsonableError from zerver.lib.exceptions import ErrorCode, JsonableError
from zerver.lib.html_diff import highlight_html_differences from zerver.lib.message import get_first_visible_message_id, messages_for_ids, render_markdown
from zerver.lib.message import (
access_message,
get_first_visible_message_id,
messages_for_ids,
render_markdown,
truncate_body,
)
from zerver.lib.queue import queue_json_publish
from zerver.lib.response import json_error, json_success from zerver.lib.response import json_error, json_success
from zerver.lib.sqlalchemy_utils import get_sqlalchemy_connection from zerver.lib.sqlalchemy_utils import get_sqlalchemy_connection
from zerver.lib.streams import ( from zerver.lib.streams import (
@ -67,14 +53,12 @@ from zerver.lib.streams import (
can_access_stream_history_by_id, can_access_stream_history_by_id,
can_access_stream_history_by_name, can_access_stream_history_by_name,
get_public_streams_queryset, get_public_streams_queryset,
get_stream_by_id,
get_stream_by_narrow_operand_access_unchecked, get_stream_by_narrow_operand_access_unchecked,
) )
from zerver.lib.timestamp import convert_to_UTC, datetime_to_timestamp from zerver.lib.timestamp import convert_to_UTC
from zerver.lib.timezone import get_timezone from zerver.lib.timezone import get_timezone
from zerver.lib.topic import ( from zerver.lib.topic import (
DB_TOPIC_NAME, DB_TOPIC_NAME,
LEGACY_PREV_TOPIC,
MATCH_TOPIC, MATCH_TOPIC,
REQ_topic, REQ_topic,
topic_column_sa, topic_column_sa,
@ -90,7 +74,6 @@ from zerver.lib.validator import (
check_list, check_list,
check_required_string, check_required_string,
check_string, check_string,
check_string_in,
check_string_or_int, check_string_or_int,
check_string_or_int_list, check_string_or_int_list,
to_non_negative_int, to_non_negative_int,
@ -1477,246 +1460,6 @@ def send_message_backend(request: HttpRequest, user_profile: UserProfile,
widget_content=widget_content) widget_content=widget_content)
return json_success({"id": ret}) return json_success({"id": ret})
def fill_edit_history_entries(message_history: List[Dict[str, Any]], message: Message) -> None:
"""This fills out the message edit history entries from the database,
which are designed to have the minimum data possible, to instead
have the current topic + content as of that time, plus data on
whatever changed. This makes it much simpler to do future
processing.
Note that this mutates what is passed to it, which is sorta a bad pattern.
"""
prev_content = message.content
prev_rendered_content = message.rendered_content
prev_topic = message.topic_name()
# Make sure that the latest entry in the history corresponds to the
# message's last edit time
if len(message_history) > 0:
assert message.last_edit_time is not None
assert(datetime_to_timestamp(message.last_edit_time) ==
message_history[0]['timestamp'])
for entry in message_history:
entry['topic'] = prev_topic
if LEGACY_PREV_TOPIC in entry:
prev_topic = entry[LEGACY_PREV_TOPIC]
entry['prev_topic'] = prev_topic
del entry[LEGACY_PREV_TOPIC]
entry['content'] = prev_content
entry['rendered_content'] = prev_rendered_content
if 'prev_content' in entry:
del entry['prev_rendered_content_version']
prev_content = entry['prev_content']
prev_rendered_content = entry['prev_rendered_content']
assert prev_rendered_content is not None
entry['content_html_diff'] = highlight_html_differences(
prev_rendered_content,
entry['rendered_content'],
message.id)
message_history.append(dict(
topic = prev_topic,
content = prev_content,
rendered_content = prev_rendered_content,
timestamp = datetime_to_timestamp(message.date_sent),
user_id = message.sender_id,
))
@has_request_variables
def get_message_edit_history(request: HttpRequest, user_profile: UserProfile,
message_id: int=REQ(converter=to_non_negative_int,
path_only=True)) -> HttpResponse:
if not user_profile.realm.allow_edit_history:
return json_error(_("Message edit history is disabled in this organization"))
message, ignored_user_message = access_message(user_profile, message_id)
# Extract the message edit history from the message
if message.edit_history is not None:
message_edit_history = ujson.loads(message.edit_history)
else:
message_edit_history = []
# Fill in all the extra data that will make it usable
fill_edit_history_entries(message_edit_history, message)
return json_success({"message_history": reversed(message_edit_history)})
PROPAGATE_MODE_VALUES = ["change_later", "change_one", "change_all"]
@has_request_variables
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()
def validate_can_delete_message(user_profile: UserProfile, message: Message) -> None:
if user_profile.is_realm_admin:
# Admin can delete any message, any time.
return
if message.sender != user_profile:
# Users can only delete messages sent by them.
raise JsonableError(_("You don't have permission to delete this message"))
if not user_profile.realm.allow_message_deleting:
# User can not delete message, if message deleting is not allowed in realm.
raise JsonableError(_("You don't have permission to delete this message"))
deadline_seconds = user_profile.realm.message_content_delete_limit_seconds
if deadline_seconds == 0:
# 0 for no time limit to delete message
return
if (timezone_now() - message.date_sent) > datetime.timedelta(seconds=deadline_seconds):
# User can not delete message after deadline time of realm
raise JsonableError(_("The time limit for deleting this message has passed"))
return
@has_request_variables
def delete_message_backend(request: HttpRequest, user_profile: UserProfile,
message_id: int=REQ(converter=to_non_negative_int,
path_only=True)) -> HttpResponse:
message, ignored_user_message = access_message(user_profile, message_id)
validate_can_delete_message(user_profile, message)
try:
do_delete_messages(user_profile.realm, [message])
except (Message.DoesNotExist, IntegrityError):
raise JsonableError(_("Message already deleted"))
return json_success()
@has_request_variables
def json_fetch_raw_message(request: HttpRequest, user_profile: UserProfile,
message_id: int=REQ(converter=to_non_negative_int,
path_only=True)) -> HttpResponse:
(message, user_message) = access_message(user_profile, message_id)
return json_success({"raw_content": message.content})
@has_request_variables @has_request_variables
def render_message_backend(request: HttpRequest, user_profile: UserProfile, def render_message_backend(request: HttpRequest, user_profile: UserProfile,
content: str=REQ()) -> HttpResponse: content: str=REQ()) -> HttpResponse:

View File

@ -602,7 +602,7 @@ class FetchLinksEmbedData(QueueProcessingWorker):
message = Message.objects.get(id=event['message_id']) message = Message.objects.get(id=event['message_id'])
# If the message changed, we will run this task after updating the message # If the message changed, we will run this task after updating the message
# in zerver.views.messages.update_message_backend # in zerver.views.message_edit.update_message_backend
if message.content != event['message_content']: if message.content != event['message_content']:
return return
if message.content is not None: if message.content is not None:

View File

@ -23,6 +23,7 @@ import zerver.views.digest
import zerver.views.documentation import zerver.views.documentation
import zerver.views.email_mirror import zerver.views.email_mirror
import zerver.views.home import zerver.views.home
import zerver.views.message_edit
import zerver.views.messages import zerver.views.messages
import zerver.views.muting import zerver.views.muting
import zerver.views.portico import zerver.views.portico
@ -189,22 +190,22 @@ v1_api_and_json_patterns = [
url(r'^zcommand$', rest_dispatch, url(r'^zcommand$', rest_dispatch,
{'POST': 'zerver.views.messages.zcommand_backend'}), {'POST': 'zerver.views.messages.zcommand_backend'}),
# messages -> zerver.views.messages # messages -> zerver.views.message*
# GET returns messages, possibly filtered, POST sends a message # GET returns messages, possibly filtered, POST sends a message
url(r'^messages$', rest_dispatch, url(r'^messages$', rest_dispatch,
{'GET': 'zerver.views.messages.get_messages_backend', {'GET': 'zerver.views.messages.get_messages_backend',
'POST': ('zerver.views.messages.send_message_backend', 'POST': ('zerver.views.messages.send_message_backend',
{'allow_incoming_webhooks'})}), {'allow_incoming_webhooks'})}),
url(r'^messages/(?P<message_id>[0-9]+)$', rest_dispatch, url(r'^messages/(?P<message_id>[0-9]+)$', rest_dispatch,
{'GET': 'zerver.views.messages.json_fetch_raw_message', {'GET': 'zerver.views.message_edit.json_fetch_raw_message',
'PATCH': 'zerver.views.messages.update_message_backend', 'PATCH': 'zerver.views.message_edit.update_message_backend',
'DELETE': 'zerver.views.messages.delete_message_backend'}), 'DELETE': 'zerver.views.message_edit.delete_message_backend'}),
url(r'^messages/render$', rest_dispatch, url(r'^messages/render$', rest_dispatch,
{'POST': 'zerver.views.messages.render_message_backend'}), {'POST': 'zerver.views.messages.render_message_backend'}),
url(r'^messages/flags$', rest_dispatch, url(r'^messages/flags$', rest_dispatch,
{'POST': 'zerver.views.messages.update_message_flags'}), {'POST': 'zerver.views.messages.update_message_flags'}),
url(r'^messages/(?P<message_id>\d+)/history$', rest_dispatch, url(r'^messages/(?P<message_id>\d+)/history$', rest_dispatch,
{'GET': 'zerver.views.messages.get_message_edit_history'}), {'GET': 'zerver.views.message_edit.get_message_edit_history'}),
url(r'^messages/matches_narrow$', rest_dispatch, url(r'^messages/matches_narrow$', rest_dispatch,
{'GET': 'zerver.views.messages.messages_in_narrow_backend'}), {'GET': 'zerver.views.messages.messages_in_narrow_backend'}),