mirror of
https://github.com/zulip/zulip.git
synced 2025-11-04 14:03:30 +00:00
schedulemessages: Add handle_deferred_message() to handle requests.
This is responsible for: 1.) Handling all the incoming requests at the messages endpoint which have defer param set. This is similar to send_message_backend apart from the fact that instead of really sending a message it schedules one to be sent later on. 2.) Does some preliminary checks such as validating timestamp for scheduling a message, prevent scheduling a message in past, ensure correct format of message to be scheduled. 3.) Extracts time of scheduled delivery from message. 4.) Add tests for the newly introduced function. 5.) timezone: Add get_timezone() to obtain tz object from string. This helps in obtaining a timezone (tz) object from a timezone specified as a string. This string needs to be a pytz lib defined timezone string which we use to specify local timezones of the users.
This commit is contained in:
@@ -5,3 +5,6 @@ import pytz
|
||||
|
||||
def get_all_timezones() -> List[Text]:
|
||||
return sorted(pytz.all_timezones)
|
||||
|
||||
def get_timezone(tz: Text) -> pytz.datetime.tzinfo:
|
||||
return pytz.timezone(tz)
|
||||
|
||||
@@ -55,11 +55,13 @@ from zerver.models import (
|
||||
Message, Realm, Recipient, Stream, UserMessage, UserProfile, Attachment,
|
||||
RealmAuditLog, RealmDomain, get_realm, UserPresence, Subscription,
|
||||
get_stream, get_stream_recipient, get_system_bot, get_user, Reaction,
|
||||
flush_per_request_caches
|
||||
flush_per_request_caches, ScheduledMessage
|
||||
)
|
||||
|
||||
|
||||
from zerver.lib.upload import create_attachment
|
||||
from zerver.lib.timestamp import convert_to_UTC
|
||||
from zerver.lib.timezone import get_timezone
|
||||
|
||||
from zerver.views.messages import create_mirrored_message_users
|
||||
|
||||
@@ -1272,6 +1274,101 @@ class MessagePOSTTest(ZulipTestCase):
|
||||
"to": "IRCLand"})
|
||||
self.assert_json_success(result)
|
||||
|
||||
class ScheduledMessageTest(ZulipTestCase):
|
||||
|
||||
def last_scheduled_message(self) -> ScheduledMessage:
|
||||
return ScheduledMessage.objects.all().order_by('-id')[0]
|
||||
|
||||
def do_schedule_message(self, msg_type: str, to: str, msg: str,
|
||||
defer_until: str, tz_guess: str='',
|
||||
realm_str: str='zulip') -> HttpResponse:
|
||||
self.login(self.example_email("hamlet"))
|
||||
|
||||
subject = ''
|
||||
if msg_type == 'stream':
|
||||
subject = 'Test subject'
|
||||
|
||||
result = self.client_post("/json/messages",
|
||||
{"type": msg_type,
|
||||
"to": to,
|
||||
"client": "test suite",
|
||||
"content": msg,
|
||||
"subject": subject,
|
||||
"realm_str": realm_str,
|
||||
"deliver_at": defer_until,
|
||||
"tz_guess": tz_guess})
|
||||
return result
|
||||
|
||||
def test_schedule_message(self) -> None:
|
||||
content = "Test message"
|
||||
defer_until = timezone_now().replace(tzinfo=None) + datetime.timedelta(days=1)
|
||||
defer_until_str = str(defer_until)
|
||||
|
||||
# Scheduling a message to a stream you are subscribed is successful.
|
||||
result = self.do_schedule_message('stream', 'Verona',
|
||||
content + ' 1', defer_until_str)
|
||||
message = self.last_scheduled_message()
|
||||
self.assert_json_success(result)
|
||||
self.assertEqual(message.content, 'Test message 1')
|
||||
self.assertEqual(message.scheduled_timestamp, convert_to_UTC(defer_until))
|
||||
|
||||
# Scheduling a private message is successful.
|
||||
result = self.do_schedule_message('private', self.example_email("othello"),
|
||||
content + ' 2', defer_until_str)
|
||||
message = self.last_scheduled_message()
|
||||
self.assert_json_success(result)
|
||||
self.assertEqual(message.content, 'Test message 2')
|
||||
self.assertEqual(message.scheduled_timestamp, convert_to_UTC(defer_until))
|
||||
|
||||
# Scheduling a message while guessing timezone.
|
||||
tz_guess = 'Asia/Kolkata'
|
||||
result = self.do_schedule_message('stream', 'Verona', content + ' 3',
|
||||
defer_until_str, tz_guess=tz_guess)
|
||||
message = self.last_scheduled_message()
|
||||
self.assert_json_success(result)
|
||||
self.assertEqual(message.content, 'Test message 3')
|
||||
local_tz = get_timezone(tz_guess)
|
||||
# Since mypy is not able to recognize localize and normalize as attributes of tzinfo we use ignore.
|
||||
utz_defer_until = local_tz.normalize(local_tz.localize(defer_until)) # type: ignore # Reason in comment on previous line.
|
||||
self.assertEqual(message.scheduled_timestamp,
|
||||
convert_to_UTC(utz_defer_until))
|
||||
|
||||
# Test with users timezone setting as set to some timezone rather than
|
||||
# empty. This will help interpret timestamp in users local timezone.
|
||||
user = self.example_user("hamlet")
|
||||
user.timezone = 'US/Pacific'
|
||||
user.save(update_fields=['timezone'])
|
||||
result = self.do_schedule_message('stream', 'Verona',
|
||||
content + ' 4', defer_until_str)
|
||||
message = self.last_scheduled_message()
|
||||
self.assert_json_success(result)
|
||||
self.assertEqual(message.content, 'Test message 4')
|
||||
local_tz = get_timezone(user.timezone)
|
||||
# Since mypy is not able to recognize localize and normalize as attributes of tzinfo we use ignore.
|
||||
utz_defer_until = local_tz.normalize(local_tz.localize(defer_until)) # type: ignore # Reason in comment on previous line.
|
||||
self.assertEqual(message.scheduled_timestamp,
|
||||
convert_to_UTC(utz_defer_until))
|
||||
|
||||
def test_scheduling_in_past(self) -> None:
|
||||
# Scheduling a message in past should fail.
|
||||
content = "Test message"
|
||||
defer_until = timezone_now()
|
||||
defer_until_str = str(defer_until)
|
||||
|
||||
result = self.do_schedule_message('stream', 'Verona',
|
||||
content + ' 1', defer_until_str)
|
||||
self.assert_json_error(result, 'Invalid timestamp for scheduling message. Choose a time in future.')
|
||||
|
||||
def test_invalid_timestamp(self) -> None:
|
||||
# Scheduling a message from which timestamp couldn't be parsed
|
||||
# successfully should fail.
|
||||
content = "Test message"
|
||||
defer_until = 'Missed the timestamp'
|
||||
|
||||
result = self.do_schedule_message('stream', 'Verona',
|
||||
content + ' 1', defer_until)
|
||||
self.assert_json_error(result, 'Invalid timestamp for scheduling message.')
|
||||
|
||||
class EditMessageTest(ZulipTestCase):
|
||||
def check_message(self, msg_id: int, subject: Optional[Text]=None,
|
||||
content: Optional[Text]=None) -> Message:
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.db import connection
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from typing import Dict, List, Set, Text, Any, Callable, Iterable, \
|
||||
Optional, Tuple, Union
|
||||
Optional, Tuple, Union, Sequence
|
||||
from zerver.lib.exceptions import JsonableError, ErrorCode
|
||||
from zerver.lib.html_diff import highlight_html_differences
|
||||
from zerver.decorator import has_request_variables, \
|
||||
@@ -18,7 +18,8 @@ from zerver.lib.actions import recipient_for_emails, do_update_message_flags, \
|
||||
compute_mit_user_fullname, compute_irc_user_fullname, compute_jabber_user_fullname, \
|
||||
create_mirror_user_if_needed, check_send_message, do_update_message, \
|
||||
extract_recipients, truncate_body, render_incoming_message, do_delete_message, \
|
||||
do_mark_all_as_read, do_mark_stream_messages_as_read, get_user_info_for_message_updates
|
||||
do_mark_all_as_read, do_mark_stream_messages_as_read, \
|
||||
get_user_info_for_message_updates, check_schedule_message
|
||||
from zerver.lib.queue import queue_json_publish
|
||||
from zerver.lib.message import (
|
||||
access_message,
|
||||
@@ -29,12 +30,13 @@ from zerver.lib.message import (
|
||||
from zerver.lib.response import json_success, json_error
|
||||
from zerver.lib.sqlalchemy_utils import get_sqlalchemy_connection
|
||||
from zerver.lib.streams import access_stream_by_id, is_public_stream_by_name
|
||||
from zerver.lib.timestamp import datetime_to_timestamp
|
||||
from zerver.lib.timestamp import datetime_to_timestamp, convert_to_UTC
|
||||
from zerver.lib.timezone import get_timezone
|
||||
from zerver.lib.topic_mutes import exclude_topic_mutes
|
||||
from zerver.lib.utils import statsd
|
||||
from zerver.lib.validator import \
|
||||
check_list, check_int, check_dict, check_string, check_bool
|
||||
from zerver.models import Message, UserProfile, Stream, Subscription, \
|
||||
from zerver.models import Message, UserProfile, Stream, Subscription, Client,\
|
||||
Realm, RealmDomain, Recipient, UserMessage, bulk_get_recipients, get_personal_recipient, \
|
||||
get_stream, email_to_domain, get_realm, get_active_streams, \
|
||||
get_user_including_cross_realm, get_stream_recipient
|
||||
@@ -43,6 +45,7 @@ from sqlalchemy import func
|
||||
from sqlalchemy.sql import select, join, column, literal_column, literal, and_, \
|
||||
or_, not_, union_all, alias, Selectable, Select, ColumnElement, table
|
||||
|
||||
from dateutil.parser import parse as dateparser
|
||||
import re
|
||||
import ujson
|
||||
import datetime
|
||||
@@ -902,6 +905,40 @@ def same_realm_jabber_user(user_profile: UserProfile, email: Text) -> bool:
|
||||
# these realms.
|
||||
return RealmDomain.objects.filter(realm=user_profile.realm, domain=domain).exists()
|
||||
|
||||
def handle_deferred_message(sender: UserProfile, client: Client,
|
||||
message_type_name: Text, message_to: Sequence[Text],
|
||||
topic_name: Optional[Text],
|
||||
message_content: Text,
|
||||
defer_until: Text, tz_guess: Text,
|
||||
forwarder_user_profile: UserProfile,
|
||||
realm: Optional[Realm]) -> HttpResponse:
|
||||
deliver_at = None
|
||||
local_tz = 'UTC'
|
||||
if tz_guess:
|
||||
local_tz = tz_guess
|
||||
elif sender.timezone:
|
||||
local_tz = sender.timezone
|
||||
try:
|
||||
deliver_at = dateparser(defer_until)
|
||||
except ValueError:
|
||||
return json_error(_("Invalid timestamp for scheduling message."))
|
||||
|
||||
deliver_at_usertz = deliver_at
|
||||
if deliver_at_usertz.tzinfo is None:
|
||||
user_tz = get_timezone(local_tz)
|
||||
# Since mypy is not able to recognize localize and normalize as attributes of tzinfo we use ignore.
|
||||
deliver_at_usertz = user_tz.normalize(user_tz.localize(deliver_at)) # type: ignore # Reason in comment on previous line.
|
||||
deliver_at = convert_to_UTC(deliver_at_usertz)
|
||||
|
||||
if deliver_at <= timezone_now():
|
||||
return json_error(_("Invalid timestamp for scheduling message. Choose a time in future."))
|
||||
|
||||
check_schedule_message(sender, client, message_type_name, message_to,
|
||||
topic_name, message_content,
|
||||
deliver_at, realm=realm,
|
||||
forwarder_user_profile=forwarder_user_profile)
|
||||
return json_success({"deliver_at": str(deliver_at_usertz)})
|
||||
|
||||
# We do not @require_login for send_message_backend, since it is used
|
||||
# both from the API and the web service. Code calling
|
||||
# send_message_backend should either check the API key or check that
|
||||
@@ -915,7 +952,9 @@ def send_message_backend(request: HttpRequest, user_profile: UserProfile,
|
||||
message_content: Text=REQ('content'),
|
||||
realm_str: Optional[Text]=REQ('realm_str', default=None),
|
||||
local_id: Optional[Text]=REQ(default=None),
|
||||
queue_id: Optional[Text]=REQ(default=None)) -> HttpResponse:
|
||||
queue_id: Optional[Text]=REQ(default=None),
|
||||
defer_until: Optional[Text]=REQ('deliver_at', default=None),
|
||||
tz_guess: Optional[Text]=REQ('tz_guess', default=None)) -> HttpResponse:
|
||||
client = request.client
|
||||
is_super_user = request.user.is_api_super_user
|
||||
if forged and not is_super_user:
|
||||
@@ -960,6 +999,13 @@ def send_message_backend(request: HttpRequest, user_profile: UserProfile,
|
||||
else:
|
||||
sender = user_profile
|
||||
|
||||
if defer_until:
|
||||
return handle_deferred_message(sender, client, message_type_name,
|
||||
message_to, topic_name, message_content,
|
||||
defer_until, tz_guess,
|
||||
forwarder_user_profile=user_profile,
|
||||
realm=realm)
|
||||
|
||||
ret = check_send_message(sender, client, message_type_name, message_to,
|
||||
topic_name, message_content, forged=forged,
|
||||
forged_timestamp = request.POST.get('time'),
|
||||
|
||||
Reference in New Issue
Block a user