2020-06-22 22:57:01 +02:00
|
|
|
import datetime
|
2021-05-09 17:24:40 +02:00
|
|
|
from typing import Any, Dict, List, Optional
|
2020-06-22 22:57:01 +02:00
|
|
|
|
2020-08-07 01:09:47 +02:00
|
|
|
import orjson
|
2021-06-03 15:46:13 +02:00
|
|
|
from django.db import IntegrityError, transaction
|
2020-06-22 22:57:01 +02:00
|
|
|
from django.http import HttpRequest, HttpResponse
|
|
|
|
from django.utils.timezone import now as timezone_now
|
2021-04-16 00:57:30 +02:00
|
|
|
from django.utils.translation import gettext as _
|
2020-06-22 22:57:01 +02:00
|
|
|
|
2021-05-09 17:24:40 +02:00
|
|
|
from zerver.lib.actions import check_update_message, do_delete_messages
|
2020-06-22 22:57:01 +02:00
|
|
|
from zerver.lib.exceptions import JsonableError
|
|
|
|
from zerver.lib.html_diff import highlight_html_differences
|
2021-05-09 17:24:40 +02:00
|
|
|
from zerver.lib.message import access_message
|
2021-08-21 19:24:20 +02:00
|
|
|
from zerver.lib.request import REQ, RequestNotes, has_request_variables
|
2021-06-30 18:35:50 +02:00
|
|
|
from zerver.lib.response import json_success
|
2020-06-22 22:57:01 +02:00
|
|
|
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
|
2021-05-09 17:24:40 +02:00
|
|
|
from zerver.models import Message, UserProfile
|
2020-06-22 22:57:01 +02:00
|
|
|
|
|
|
|
|
|
|
|
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
|
2021-02-12 08:20:45 +01:00
|
|
|
assert datetime_to_timestamp(message.last_edit_time) == message_history[0]["timestamp"]
|
2020-06-22 22:57:01 +02:00
|
|
|
|
|
|
|
for entry in message_history:
|
2021-02-12 08:20:45 +01:00
|
|
|
entry["topic"] = prev_topic
|
2020-06-22 22:57:01 +02:00
|
|
|
if LEGACY_PREV_TOPIC in entry:
|
|
|
|
prev_topic = entry[LEGACY_PREV_TOPIC]
|
2021-02-12 08:20:45 +01:00
|
|
|
entry["prev_topic"] = prev_topic
|
2020-06-22 22:57:01 +02:00
|
|
|
del entry[LEGACY_PREV_TOPIC]
|
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
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"]
|
2020-06-22 22:57:01 +02:00
|
|
|
assert prev_rendered_content is not None
|
2021-02-12 08:20:45 +01:00
|
|
|
entry["content_html_diff"] = highlight_html_differences(
|
|
|
|
prev_rendered_content, entry["rendered_content"], message.id
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
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,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2020-06-22 22:57:01 +02:00
|
|
|
|
|
|
|
@has_request_variables
|
2021-02-12 08:19:30 +01:00
|
|
|
def get_message_edit_history(
|
|
|
|
request: HttpRequest,
|
|
|
|
user_profile: UserProfile,
|
|
|
|
message_id: int = REQ(converter=to_non_negative_int, path_only=True),
|
|
|
|
) -> HttpResponse:
|
2020-06-22 22:57:01 +02:00
|
|
|
if not user_profile.realm.allow_edit_history:
|
2021-06-30 18:35:50 +02:00
|
|
|
raise JsonableError(_("Message edit history is disabled in this organization"))
|
2020-06-22 22:57:01 +02:00
|
|
|
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:
|
2020-08-07 01:09:47 +02:00
|
|
|
message_edit_history = orjson.loads(message.edit_history)
|
2020-06-22 22:57:01 +02:00
|
|
|
else:
|
|
|
|
message_edit_history = []
|
|
|
|
|
|
|
|
# Fill in all the extra data that will make it usable
|
|
|
|
fill_edit_history_entries(message_edit_history, message)
|
2020-08-07 06:52:22 +02:00
|
|
|
return json_success({"message_history": list(reversed(message_edit_history))})
|
2020-06-22 22:57:01 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2020-06-22 22:57:01 +02:00
|
|
|
PROPAGATE_MODE_VALUES = ["change_later", "change_one", "change_all"]
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
|
2020-06-22 22:57:01 +02:00
|
|
|
@has_request_variables
|
2021-02-12 08:19:30 +01:00
|
|
|
def update_message_backend(
|
|
|
|
request: HttpRequest,
|
2021-04-01 15:50:03 +02:00
|
|
|
user_profile: UserProfile,
|
2021-02-12 08:19:30 +01:00
|
|
|
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(),
|
2021-08-19 10:45:20 +02:00
|
|
|
propagate_mode: str = REQ(
|
2021-02-12 08:19:30 +01:00
|
|
|
default="change_one", str_validator=check_string_in(PROPAGATE_MODE_VALUES)
|
|
|
|
),
|
2021-04-07 22:00:44 +02:00
|
|
|
send_notification_to_old_thread: bool = REQ(default=True, json_validator=check_bool),
|
|
|
|
send_notification_to_new_thread: bool = REQ(default=True, json_validator=check_bool),
|
2021-02-12 08:19:30 +01:00
|
|
|
content: Optional[str] = REQ(default=None),
|
|
|
|
) -> HttpResponse:
|
2021-05-09 17:24:40 +02:00
|
|
|
number_changed = check_update_message(
|
2021-02-12 08:19:30 +01:00
|
|
|
user_profile,
|
2021-05-09 17:24:40 +02:00
|
|
|
message_id,
|
|
|
|
stream_id,
|
2021-02-12 08:19:30 +01:00
|
|
|
topic_name,
|
|
|
|
propagate_mode,
|
|
|
|
send_notification_to_old_thread,
|
|
|
|
send_notification_to_new_thread,
|
|
|
|
content,
|
|
|
|
)
|
2020-06-22 22:57:01 +02:00
|
|
|
|
|
|
|
# Include the number of messages changed in the logs
|
2021-08-21 19:24:20 +02:00
|
|
|
log_data = RequestNotes.get_notes(request).log_data
|
2021-07-09 10:06:04 +02:00
|
|
|
assert log_data is not None
|
|
|
|
log_data["extra"] = f"[{number_changed}]"
|
2021-05-09 17:24:40 +02:00
|
|
|
|
2020-06-22 22:57:01 +02:00
|
|
|
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
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2021-06-03 15:46:13 +02:00
|
|
|
@transaction.atomic
|
2020-06-22 22:57:01 +02:00
|
|
|
@has_request_variables
|
2021-02-12 08:19:30 +01:00
|
|
|
def delete_message_backend(
|
|
|
|
request: HttpRequest,
|
|
|
|
user_profile: UserProfile,
|
|
|
|
message_id: int = REQ(converter=to_non_negative_int, path_only=True),
|
|
|
|
) -> HttpResponse:
|
2021-06-03 15:46:13 +02:00
|
|
|
# We lock the `Message` object to ensure that any transactions modifying the `Message` object
|
|
|
|
# concurrently are serialized properly with deleting the message; this prevents a deadlock
|
|
|
|
# that would otherwise happen because of the other transaction holding a lock on the `Message`
|
|
|
|
# row.
|
|
|
|
message, ignored_user_message = access_message(user_profile, message_id, lock_message=True)
|
2020-06-22 22:57:01 +02:00
|
|
|
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()
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2020-06-22 22:57:01 +02:00
|
|
|
@has_request_variables
|
2021-02-12 08:19:30 +01:00
|
|
|
def json_fetch_raw_message(
|
|
|
|
request: HttpRequest,
|
|
|
|
user_profile: UserProfile,
|
|
|
|
message_id: int = REQ(converter=to_non_negative_int, path_only=True),
|
|
|
|
) -> HttpResponse:
|
2020-06-22 22:57:01 +02:00
|
|
|
(message, user_message) = access_message(user_profile, message_id)
|
|
|
|
return json_success({"raw_content": message.content})
|