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:
Aditya Bansal
2018-01-04 23:11:34 +05:30
committed by showell
parent 6b03e25382
commit f46d098558
3 changed files with 152 additions and 6 deletions

View File

@@ -5,3 +5,6 @@ import pytz
def get_all_timezones() -> List[Text]: def get_all_timezones() -> List[Text]:
return sorted(pytz.all_timezones) return sorted(pytz.all_timezones)
def get_timezone(tz: Text) -> pytz.datetime.tzinfo:
return pytz.timezone(tz)

View File

@@ -55,11 +55,13 @@ from zerver.models import (
Message, Realm, Recipient, Stream, UserMessage, UserProfile, Attachment, Message, Realm, Recipient, Stream, UserMessage, UserProfile, Attachment,
RealmAuditLog, RealmDomain, get_realm, UserPresence, Subscription, RealmAuditLog, RealmDomain, get_realm, UserPresence, Subscription,
get_stream, get_stream_recipient, get_system_bot, get_user, Reaction, 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.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 from zerver.views.messages import create_mirrored_message_users
@@ -1272,6 +1274,101 @@ class MessagePOSTTest(ZulipTestCase):
"to": "IRCLand"}) "to": "IRCLand"})
self.assert_json_success(result) 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): class EditMessageTest(ZulipTestCase):
def check_message(self, msg_id: int, subject: Optional[Text]=None, def check_message(self, msg_id: int, subject: Optional[Text]=None,
content: Optional[Text]=None) -> Message: content: Optional[Text]=None) -> Message:

View File

@@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError
from django.db import connection from django.db import connection
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from typing import Dict, List, Set, Text, Any, Callable, Iterable, \ 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.exceptions import JsonableError, ErrorCode
from zerver.lib.html_diff import highlight_html_differences from zerver.lib.html_diff import highlight_html_differences
from zerver.decorator import has_request_variables, \ 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, \ compute_mit_user_fullname, compute_irc_user_fullname, compute_jabber_user_fullname, \
create_mirror_user_if_needed, check_send_message, do_update_message, \ create_mirror_user_if_needed, check_send_message, do_update_message, \
extract_recipients, truncate_body, render_incoming_message, do_delete_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.queue import queue_json_publish
from zerver.lib.message import ( from zerver.lib.message import (
access_message, access_message,
@@ -29,12 +30,13 @@ from zerver.lib.message import (
from zerver.lib.response import json_success, json_error from zerver.lib.response import json_success, json_error
from zerver.lib.sqlalchemy_utils import get_sqlalchemy_connection 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.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.topic_mutes import exclude_topic_mutes
from zerver.lib.utils import statsd from zerver.lib.utils import statsd
from zerver.lib.validator import \ from zerver.lib.validator import \
check_list, check_int, check_dict, check_string, check_bool 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, \ Realm, RealmDomain, Recipient, UserMessage, bulk_get_recipients, get_personal_recipient, \
get_stream, email_to_domain, get_realm, get_active_streams, \ get_stream, email_to_domain, get_realm, get_active_streams, \
get_user_including_cross_realm, get_stream_recipient 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_, \ from sqlalchemy.sql import select, join, column, literal_column, literal, and_, \
or_, not_, union_all, alias, Selectable, Select, ColumnElement, table or_, not_, union_all, alias, Selectable, Select, ColumnElement, table
from dateutil.parser import parse as dateparser
import re import re
import ujson import ujson
import datetime import datetime
@@ -902,6 +905,40 @@ def same_realm_jabber_user(user_profile: UserProfile, email: Text) -> bool:
# these realms. # these realms.
return RealmDomain.objects.filter(realm=user_profile.realm, domain=domain).exists() 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 # We do not @require_login for send_message_backend, since it is used
# both from the API and the web service. Code calling # both from the API and the web service. Code calling
# send_message_backend should either check the API key or check that # 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'), message_content: Text=REQ('content'),
realm_str: Optional[Text]=REQ('realm_str', default=None), realm_str: Optional[Text]=REQ('realm_str', default=None),
local_id: Optional[Text]=REQ(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 client = request.client
is_super_user = request.user.is_api_super_user is_super_user = request.user.is_api_super_user
if forged and not is_super_user: if forged and not is_super_user:
@@ -960,6 +999,13 @@ def send_message_backend(request: HttpRequest, user_profile: UserProfile,
else: else:
sender = user_profile 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, ret = check_send_message(sender, client, message_type_name, message_to,
topic_name, message_content, forged=forged, topic_name, message_content, forged=forged,
forged_timestamp = request.POST.get('time'), forged_timestamp = request.POST.get('time'),