mirror of https://github.com/zulip/zulip.git
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:
parent
1a6799f15e
commit
4d7550d705
|
@ -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
|
||||
// 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
|
||||
// zerver.views.messages.update_message_backend
|
||||
// zerver.views.message_edit.update_message_backend
|
||||
const seconds_left_buffer = 5;
|
||||
const editability = get_editability(message, seconds_left_buffer);
|
||||
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) {
|
||||
// Give them at least 10 seconds.
|
||||
// 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 now = new XDate();
|
||||
let seconds_left = page_params.realm_message_content_edit_limit_seconds +
|
||||
|
|
|
@ -283,7 +283,7 @@ class PreviewTestCase(ZulipTestCase):
|
|||
url = 'http://test.org/'
|
||||
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), {
|
||||
'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>',
|
||||
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), {
|
||||
'message_id': msg_id, 'content': edited_url,
|
||||
})
|
||||
|
|
|
@ -4629,9 +4629,9 @@ class DeleteMessageTest(ZulipTestCase):
|
|||
|
||||
# Test handling of 500 error caused by multiple delete requests due to latency.
|
||||
# see issue #11219.
|
||||
with mock.patch("zerver.views.messages.do_delete_messages") as m, \
|
||||
mock.patch("zerver.views.messages.validate_can_delete_message", return_value=None), \
|
||||
mock.patch("zerver.views.messages.access_message", return_value=(None, None)):
|
||||
with mock.patch("zerver.views.message_edit.do_delete_messages") as m, \
|
||||
mock.patch("zerver.views.message_edit.validate_can_delete_message", return_value=None), \
|
||||
mock.patch("zerver.views.message_edit.access_message", return_value=(None, None)):
|
||||
m.side_effect = IntegrityError()
|
||||
result = test_delete_message_by_owner(msg_id=msg_id)
|
||||
self.assert_json_error(result, "Message already deleted")
|
||||
|
|
|
@ -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})
|
|
@ -1,13 +1,12 @@
|
|||
import datetime
|
||||
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
|
||||
from dateutil.parser import parse as dateparser
|
||||
from django.conf import settings
|
||||
from django.core import validators
|
||||
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.utils.html import escape as escape_html
|
||||
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.lib import bugdown
|
||||
from zerver.lib.actions import (
|
||||
check_schedule_message,
|
||||
check_send_message,
|
||||
compute_irc_user_fullname,
|
||||
compute_jabber_user_fullname,
|
||||
create_mirror_user_if_needed,
|
||||
do_delete_messages,
|
||||
do_mark_all_as_read,
|
||||
do_mark_stream_messages_as_read,
|
||||
do_update_message,
|
||||
do_update_message_flags,
|
||||
extract_private_recipients,
|
||||
extract_stream_indicator,
|
||||
get_user_info_for_message_updates,
|
||||
recipient_for_user_profiles,
|
||||
render_incoming_message,
|
||||
)
|
||||
from zerver.lib.addressee import get_user_profiles, get_user_profiles_by_ids
|
||||
from zerver.lib.exceptions import ErrorCode, JsonableError
|
||||
from zerver.lib.html_diff import highlight_html_differences
|
||||
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.message import get_first_visible_message_id, messages_for_ids, render_markdown
|
||||
from zerver.lib.response import json_error, json_success
|
||||
from zerver.lib.sqlalchemy_utils import get_sqlalchemy_connection
|
||||
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_name,
|
||||
get_public_streams_queryset,
|
||||
get_stream_by_id,
|
||||
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.topic import (
|
||||
DB_TOPIC_NAME,
|
||||
LEGACY_PREV_TOPIC,
|
||||
MATCH_TOPIC,
|
||||
REQ_topic,
|
||||
topic_column_sa,
|
||||
|
@ -90,7 +74,6 @@ from zerver.lib.validator import (
|
|||
check_list,
|
||||
check_required_string,
|
||||
check_string,
|
||||
check_string_in,
|
||||
check_string_or_int,
|
||||
check_string_or_int_list,
|
||||
to_non_negative_int,
|
||||
|
@ -1477,246 +1460,6 @@ def send_message_backend(request: HttpRequest, user_profile: UserProfile,
|
|||
widget_content=widget_content)
|
||||
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
|
||||
def render_message_backend(request: HttpRequest, user_profile: UserProfile,
|
||||
content: str=REQ()) -> HttpResponse:
|
||||
|
|
|
@ -602,7 +602,7 @@ class FetchLinksEmbedData(QueueProcessingWorker):
|
|||
|
||||
message = Message.objects.get(id=event['message_id'])
|
||||
# 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']:
|
||||
return
|
||||
if message.content is not None:
|
||||
|
|
|
@ -23,6 +23,7 @@ import zerver.views.digest
|
|||
import zerver.views.documentation
|
||||
import zerver.views.email_mirror
|
||||
import zerver.views.home
|
||||
import zerver.views.message_edit
|
||||
import zerver.views.messages
|
||||
import zerver.views.muting
|
||||
import zerver.views.portico
|
||||
|
@ -189,22 +190,22 @@ v1_api_and_json_patterns = [
|
|||
url(r'^zcommand$', rest_dispatch,
|
||||
{'POST': 'zerver.views.messages.zcommand_backend'}),
|
||||
|
||||
# messages -> zerver.views.messages
|
||||
# messages -> zerver.views.message*
|
||||
# GET returns messages, possibly filtered, POST sends a message
|
||||
url(r'^messages$', rest_dispatch,
|
||||
{'GET': 'zerver.views.messages.get_messages_backend',
|
||||
'POST': ('zerver.views.messages.send_message_backend',
|
||||
{'allow_incoming_webhooks'})}),
|
||||
url(r'^messages/(?P<message_id>[0-9]+)$', rest_dispatch,
|
||||
{'GET': 'zerver.views.messages.json_fetch_raw_message',
|
||||
'PATCH': 'zerver.views.messages.update_message_backend',
|
||||
'DELETE': 'zerver.views.messages.delete_message_backend'}),
|
||||
{'GET': 'zerver.views.message_edit.json_fetch_raw_message',
|
||||
'PATCH': 'zerver.views.message_edit.update_message_backend',
|
||||
'DELETE': 'zerver.views.message_edit.delete_message_backend'}),
|
||||
url(r'^messages/render$', rest_dispatch,
|
||||
{'POST': 'zerver.views.messages.render_message_backend'}),
|
||||
url(r'^messages/flags$', rest_dispatch,
|
||||
{'POST': 'zerver.views.messages.update_message_flags'}),
|
||||
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,
|
||||
{'GET': 'zerver.views.messages.messages_in_narrow_backend'}),
|
||||
|
||||
|
|
Loading…
Reference in New Issue