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
|
// 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 +
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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'}),
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue