mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-03 21:43:21 +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