reminders: Add API endpoint to schedule reminders.

This commit is contained in:
Aman Agrawal
2025-05-02 10:30:54 +05:30
committed by Tim Abbott
parent ad9cb50183
commit 733817cb51
17 changed files with 556 additions and 12 deletions

View File

@@ -20,6 +20,11 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 11.0
**Feature level 381**
* [`POST /reminders`](/api/create-message-reminder): Added a new endpoint to
schedule personal reminder for a message.
**Feature level 380**
* [`POST /register`](/api/register-queue), [`GET

View File

@@ -26,6 +26,10 @@
* [Edit a scheduled message](/api/update-scheduled-message)
* [Delete a scheduled message](/api/delete-scheduled-message)
#### Message reminders
* [Create a message reminder](/api/create-message-reminder)
#### Drafts
* [Get drafts](/api/get-drafts)

View File

@@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
# new level means in api_docs/changelog.md, as well as "**Changes**"
# entries in the endpoint's documentation in `zulip.yaml`.
API_FEATURE_LEVEL = 380
API_FEATURE_LEVEL = 381
# Bump the minor PROVISION_VERSION to indicate that folks should provision
# only when going from an old version of the code to a newer version. Bump

View File

@@ -290,6 +290,7 @@ export function quote_message(opts: {
// ```quote
// message content
// ```
// Keep syntax in sync with zerver/lib/reminders.py
let content = $t(
{defaultMessage: "{username} [said]({link_to_message}):"},
{

View File

@@ -0,0 +1,39 @@
import datetime
from zerver.actions.message_send import check_message
from zerver.actions.scheduled_messages import do_schedule_messages
from zerver.lib.addressee import Addressee
from zerver.lib.message import access_message
from zerver.lib.reminders import get_reminder_formatted_content
from zerver.models import Client, ScheduledMessage, UserProfile
def schedule_reminder_for_message(
current_user: UserProfile,
client: Client,
message_id: int,
deliver_at: datetime.datetime,
) -> int:
message = access_message(current_user, message_id, is_modifying_message=False)
# Even though reminder will be sent from NOTIFICATION_BOT, we still
# set current_user as the sender here to help us make the permission checks easier.
addressee = Addressee.for_user_profile(current_user)
# This can raise an exception in the unlikely event that the current user cannot DM themself.
send_request = check_message(
current_user,
client,
addressee,
get_reminder_formatted_content(message, current_user),
current_user.realm,
forwarder_user_profile=current_user,
)
send_request.deliver_at = deliver_at
send_request.reminder_target_message_id = message_id
return do_schedule_messages(
[send_request],
current_user,
read_by_sender=False,
skip_events=True,
delivery_type=ScheduledMessage.REMIND,
)[0]

View File

@@ -23,8 +23,9 @@ from zerver.lib.exceptions import (
UserDeactivatedError,
)
from zerver.lib.markdown import render_message_markdown
from zerver.lib.message import SendMessageRequest, truncate_topic
from zerver.lib.message import SendMessageRequest, access_message, truncate_topic
from zerver.lib.recipient_parsing import extract_direct_message_recipient_ids, extract_stream_id
from zerver.lib.reminders import get_reminder_formatted_content
from zerver.lib.scheduled_messages import access_scheduled_message
from zerver.lib.string_validation import check_stream_topic
from zerver.models import Client, Realm, ScheduledMessage, Subscription, UserProfile
@@ -68,7 +69,11 @@ def check_schedule_message(
)
return do_schedule_messages(
[send_request], sender, read_by_sender=read_by_sender, skip_events=skip_events
[send_request],
sender,
read_by_sender=read_by_sender,
skip_events=skip_events,
delivery_type=ScheduledMessage.SEND_LATER,
)[0]
@@ -78,6 +83,7 @@ def do_schedule_messages(
*,
read_by_sender: bool = False,
skip_events: bool = False,
delivery_type: int,
) -> list[int]:
scheduled_messages: list[tuple[ScheduledMessage, SendMessageRequest]] = []
@@ -98,7 +104,10 @@ def do_schedule_messages(
assert send_request.deliver_at is not None
scheduled_message.scheduled_timestamp = send_request.deliver_at
scheduled_message.read_by_sender = read_by_sender
scheduled_message.delivery_type = ScheduledMessage.SEND_LATER
scheduled_message.delivery_type = delivery_type
if delivery_type == ScheduledMessage.REMIND:
scheduled_message.reminder_target_message_id = send_request.reminder_target_message_id
scheduled_messages.append((scheduled_message, send_request))
@@ -114,6 +123,7 @@ def do_schedule_messages(
scheduled_message.save(update_fields=["has_attachment"])
if not skip_events:
assert delivery_type == ScheduledMessage.SEND_LATER
event = {
"type": "scheduled_messages",
"op": "add",
@@ -275,12 +285,35 @@ def delete_scheduled_message(user_profile: UserProfile, scheduled_message_id: in
notify_remove_scheduled_message(user_profile, scheduled_message_id)
def send_reminder(scheduled_message: ScheduledMessage) -> None:
message_id = scheduled_message.reminder_target_message_id
assert message_id is not None
current_user = scheduled_message.sender
try:
message = access_message(current_user, message_id, is_modifying_message=False)
content = get_reminder_formatted_content(message, current_user)
except JsonableError:
# If we no longer have access to the message, we send the reminder with the
# last known message position and content.
content = scheduled_message.content
# Reminder messages are always sent from the notification bot.
message_id = internal_send_private_message(
get_system_bot(settings.NOTIFICATION_BOT, scheduled_message.realm.id),
current_user,
content,
)
scheduled_message.delivered_message_id = message_id
scheduled_message.delivered = True
scheduled_message.save(update_fields=["delivered", "delivered_message_id"])
def send_scheduled_message(scheduled_message: ScheduledMessage) -> None:
assert not scheduled_message.delivered
assert not scheduled_message.failed
# It's currently not possible to use the reminder feature.
assert scheduled_message.delivery_type == ScheduledMessage.SEND_LATER
if scheduled_message.delivery_type == ScheduledMessage.REMIND:
send_reminder(scheduled_message)
return
# Repeat the checks from validate_account_and_subdomain, in case
# the state changed since the message as scheduled.
@@ -418,6 +451,8 @@ def try_deliver_one_scheduled_message() -> bool:
if (
not was_delivered
# Reminders have their own notification system.
and scheduled_message.delivery_type != ScheduledMessage.REMIND
# Do not send notification if either the realm or
# the sending user account has been deactivated.
and not isinstance(e, RealmDeactivatedError)

View File

@@ -147,6 +147,20 @@ CODE_VALIDATORS: dict[str | None, Callable[[list[str]], None]] = {
}
# This function is similar to one used in fenced_code.ts
def get_unused_fence(content: str) -> str:
# Define the regular expression pattern to match ``` fences
fence_length_re = re.compile(r"^ {0,3}(`{3,})", re.MULTILINE)
# Initialize the length variable to 3, corresponding to default fence length
length = 3
matches = fence_length_re.findall(content)
for match in matches:
length = max(length, len(match) + 1)
return "`" * length
class FencedCodeExtension(Extension):
def __init__(self, config: Mapping[str, Any] = {}) -> None:
self.config = {

View File

@@ -181,6 +181,7 @@ class SendMessageRequest:
disable_external_notifications: bool = False
automatic_new_visibility_policy: int | None = None
recipients_for_user_creation_events: dict[UserProfile, set[int]] | None = None
reminder_target_message_id: int | None = None
# We won't try to fetch more unread message IDs from the database than

51
zerver/lib/reminders.py Normal file
View File

@@ -0,0 +1,51 @@
from django.conf import settings
from django.utils.translation import gettext as _
from zerver.lib.markdown.fenced_code import get_unused_fence
from zerver.lib.mention import silent_mention_syntax_for_user
from zerver.lib.message import truncate_content
from zerver.lib.message_cache import MessageDict
from zerver.lib.url_encoding import near_message_url, topic_narrow_url
from zerver.models import Message, Stream, UserProfile
def get_reminder_formatted_content(message: Message, current_user: UserProfile) -> str:
if message.is_stream_message():
# We don't need to check access here since we already have the message
# whose access has already been checked by the caller.
stream = Stream.objects.get(
id=message.recipient.type_id,
realm=current_user.realm,
)
narrow_link = topic_narrow_url(
realm=current_user.realm,
stream=stream,
topic_name=message.topic_name(),
)
content = _(
"You requested a reminder for the following message sent to [{stream_name} > {topic_name}]({narrow_link})."
).format(
stream_name=stream.name,
topic_name=message.topic_name(),
narrow_link=narrow_link,
)
else:
content = _("You requested a reminder for the following direct message.")
# Format the message content as a quote.
content += "\n\n"
content += _("{user_silent_mention} [said]({conversation_url}):").format(
user_silent_mention=silent_mention_syntax_for_user(message.sender),
conversation_url=near_message_url(current_user.realm, MessageDict.wide_dict(message)),
)
content += "\n"
fence = get_unused_fence(content)
quoted_message = "{fence}quote\n{msg_content}\n{fence}"
content += quoted_message
length_without_message_content = len(content.format(fence=fence, msg_content=""))
max_length = settings.MAX_MESSAGE_LENGTH - length_without_message_content
msg_content = truncate_content(message.content, max_length, "\n[message truncated]")
return content.format(
fence=fence,
msg_content=msg_content,
)

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.1.8 on 2025-04-28 08:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0698_scheduledmessage_request_timestamp"),
]
operations = [
migrations.AddField(
model_name="scheduledmessage",
name="reminder_target_message_id",
field=models.IntegerField(null=True),
),
]

View File

@@ -153,6 +153,8 @@ class ScheduledMessage(models.Model):
delivered_message = models.ForeignKey(Message, null=True, on_delete=CASCADE)
has_attachment = models.BooleanField(default=False, db_index=True)
request_timestamp = models.DateTimeField(default=timezone_now)
# Only used for REMIND delivery_type messages.
reminder_target_message_id = models.IntegerField(null=True)
# Metadata for messages that failed to send when their scheduled
# moment arrived.

View File

@@ -6560,6 +6560,51 @@ paths:
"result": "error",
"msg": "Saved snippet does not exist.",
}
/reminders:
post:
operationId: create-message-reminder
tags: ["reminders"]
summary: Create a message reminder
description: |
Schedule a reminder to be sent to the current user at the specified time. The reminder will link the relevant message.
**Changes**: New in Zulip 11.0 (feature level ZF-f8d751).
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
message_id:
description: |
The ID of the previously sent message to reference in the reminder message.
type: integer
example: 1
scheduled_delivery_timestamp:
type: integer
description: |
The UNIX timestamp for when the reminder will be sent,
in UTC seconds.
example: 5681662420
responses:
"200":
description: Success.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/JsonSuccessBase"
- additionalProperties: false
properties:
result: {}
msg: {}
ignored_parameters_unsupported: {}
reminder_id:
type: integer
description: |
Unique ID of the scheduled message reminder.
example: {"msg": "", "reminder_id": 42, "result": "success"}
/scheduled_messages:
get:
operationId: get-scheduled-messages

View File

@@ -912,6 +912,7 @@ class OpenAPIAttributesTest(ZulipTestCase):
"scheduled_messages",
"mobile",
"invites",
"reminders",
]
paths = OpenAPISpec(OPENAPI_SPEC_PATH).openapi()["paths"]
for path, path_item in paths.items():

View File

@@ -0,0 +1,289 @@
import datetime
import time
from typing import TYPE_CHECKING
import time_machine
from zerver.actions.scheduled_messages import try_deliver_one_scheduled_message
from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.timestamp import timestamp_to_datetime
from zerver.models import Message, ScheduledMessage
if TYPE_CHECKING:
from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse
class RemindersTest(ZulipTestCase):
def do_schedule_reminder(
self,
message_id: int,
scheduled_delivery_timestamp: int,
) -> "TestHttpResponse":
self.login("hamlet")
payload = {
"message_id": message_id,
"scheduled_delivery_timestamp": scheduled_delivery_timestamp,
}
result = self.client_post("/json/reminders", payload)
return result
def create_reminder(self, content: str, message_type: str = "direct") -> ScheduledMessage:
if message_type == "stream":
message_id = self.send_channel_message_for_hamlet(content)
else:
message_id = self.send_dm_from_hamlet_to_othello(content)
scheduled_delivery_timestamp = int(time.time() + 86400)
result = self.do_schedule_reminder(message_id, scheduled_delivery_timestamp)
self.assert_json_success(result)
return self.last_scheduled_reminder()
def last_scheduled_reminder(self) -> ScheduledMessage:
return ScheduledMessage.objects.filter(delivery_type=ScheduledMessage.REMIND).order_by(
"-id"
)[0]
def send_channel_message_for_hamlet(self, content: str) -> int:
return self.send_stream_message(self.example_user("hamlet"), "Verona", content)
def send_dm_from_hamlet_to_othello(self, content: str) -> int:
return self.send_personal_message(
self.example_user("hamlet"), self.example_user("othello"), content
)
def get_dm_reminder_content(self, msg_content: str, msg_id: int) -> str:
return (
"You requested a reminder for the following direct message.\n\n"
f"@_**King Hamlet|10** [said](http://zulip.testserver/#narrow/dm/10,12-pm/near/{msg_id}):\n```quote\n{msg_content}\n```"
)
def get_channel_message_reminder_content(self, msg_content: str, msg_id: int) -> str:
return (
"You requested a reminder for the following message sent to [Verona > test](http://zulip.testserver/#narrow/channel/3-Verona/topic/test).\n\n"
f"@_**King Hamlet|10** [said](http://zulip.testserver/#narrow/channel/3-Verona/topic/test/near/{msg_id}):\n```quote\n{msg_content}\n```"
)
def test_schedule_reminder(self) -> None:
self.login("hamlet")
content = "Test message"
scheduled_delivery_timestamp = int(time.time() + 86400)
# Scheduling a reminder to a channel you are subscribed is successful.
message_id = self.send_channel_message_for_hamlet(content)
result = self.do_schedule_reminder(message_id, scheduled_delivery_timestamp)
self.assert_json_success(result)
scheduled_message = self.last_scheduled_reminder()
self.assertEqual(
scheduled_message.content,
self.get_channel_message_reminder_content(content, message_id),
)
# Recipient and sender are the same for reminders.
self.assertEqual(scheduled_message.recipient.type_id, self.example_user("hamlet").id)
self.assertEqual(scheduled_message.sender, self.example_user("hamlet"))
self.assertEqual(
scheduled_message.scheduled_timestamp,
timestamp_to_datetime(scheduled_delivery_timestamp),
)
self.assertEqual(
scheduled_message.reminder_target_message_id,
message_id,
)
# Scheduling a direct message with user IDs is successful.
self.example_user("othello")
message_id = self.send_dm_from_hamlet_to_othello(content)
result = self.do_schedule_reminder(message_id, scheduled_delivery_timestamp)
self.assert_json_success(result)
scheduled_message = self.last_scheduled_reminder()
self.assertEqual(
scheduled_message.content, self.get_dm_reminder_content(content, message_id)
)
self.assertEqual(scheduled_message.recipient.type_id, self.example_user("hamlet").id)
self.assertEqual(scheduled_message.sender, self.example_user("hamlet"))
self.assertEqual(
scheduled_message.scheduled_timestamp,
timestamp_to_datetime(scheduled_delivery_timestamp),
)
self.assertEqual(
scheduled_message.reminder_target_message_id,
message_id,
)
def test_schedule_reminder_with_bad_timestamp(self) -> None:
self.login("hamlet")
content = "Test message"
scheduled_delivery_timestamp = int(time.time() - 86400)
message_id = self.send_channel_message_for_hamlet(content)
result = self.do_schedule_reminder(message_id, scheduled_delivery_timestamp)
self.assert_json_error(result, "Scheduled delivery time must be in the future.")
def test_schedule_reminder_with_bad_message_id(self) -> None:
self.login("hamlet")
scheduled_delivery_timestamp = int(time.time() + 86400)
result = self.do_schedule_reminder(123456789, scheduled_delivery_timestamp)
self.assert_json_error(result, "Invalid message(s)")
def test_successful_deliver_direct_message_reminder(self) -> None:
# No scheduled message
result = try_deliver_one_scheduled_message()
self.assertFalse(result)
content = "Test content"
reminder = self.create_reminder(content)
# mock current time to be greater than the scheduled time, so that the `scheduled_message` can be sent.
more_than_scheduled_delivery_datetime = reminder.scheduled_timestamp + datetime.timedelta(
minutes=1
)
with (
time_machine.travel(more_than_scheduled_delivery_datetime, tick=False),
self.assertLogs(level="INFO") as logs,
):
result = try_deliver_one_scheduled_message()
self.assertTrue(result)
reminder.refresh_from_db()
self.assertEqual(
logs.output,
[
f"INFO:root:Sending scheduled message {reminder.id} with date {reminder.scheduled_timestamp} (sender: {reminder.sender_id})"
],
)
self.assertEqual(reminder.delivered, True)
self.assertEqual(reminder.failed, False)
assert isinstance(reminder.delivered_message_id, int)
delivered_message = Message.objects.get(id=reminder.delivered_message_id)
assert isinstance(reminder.reminder_target_message_id, int)
self.assertEqual(
delivered_message.content,
self.get_dm_reminder_content(content, reminder.reminder_target_message_id),
)
self.assertEqual(delivered_message.date_sent, more_than_scheduled_delivery_datetime)
def test_successful_deliver_channel_message_reminder(self) -> None:
# No scheduled message
result = try_deliver_one_scheduled_message()
self.assertFalse(result)
content = "Test content"
reminder = self.create_reminder(content, "stream")
# mock current time to be greater than the scheduled time, so that the `scheduled_message` can be sent.
more_than_scheduled_delivery_datetime = reminder.scheduled_timestamp + datetime.timedelta(
minutes=1
)
with (
time_machine.travel(more_than_scheduled_delivery_datetime, tick=False),
self.assertLogs(level="INFO") as logs,
):
result = try_deliver_one_scheduled_message()
self.assertTrue(result)
reminder.refresh_from_db()
self.assertEqual(
logs.output,
[
f"INFO:root:Sending scheduled message {reminder.id} with date {reminder.scheduled_timestamp} (sender: {reminder.sender_id})"
],
)
self.assertEqual(reminder.delivered, True)
self.assertEqual(reminder.failed, False)
assert isinstance(reminder.delivered_message_id, int)
delivered_message = Message.objects.get(id=reminder.delivered_message_id)
assert isinstance(reminder.reminder_target_message_id, int)
self.assertEqual(
delivered_message.content,
self.get_channel_message_reminder_content(
content, reminder.reminder_target_message_id
),
)
self.assertEqual(delivered_message.date_sent, more_than_scheduled_delivery_datetime)
def test_send_reminder_at_max_content_limit(self) -> None:
# No scheduled message
result = try_deliver_one_scheduled_message()
self.assertFalse(result)
content = "x" * 10000
reminder = self.create_reminder(content)
# mock current time to be greater than the scheduled time, so that the `scheduled_message` can be sent.
more_than_scheduled_delivery_datetime = reminder.scheduled_timestamp + datetime.timedelta(
minutes=1
)
with (
time_machine.travel(more_than_scheduled_delivery_datetime, tick=False),
self.assertLogs(level="INFO") as logs,
):
result = try_deliver_one_scheduled_message()
self.assertTrue(result)
reminder.refresh_from_db()
self.assertEqual(
logs.output,
[
f"INFO:root:Sending scheduled message {reminder.id} with date {reminder.scheduled_timestamp} (sender: {reminder.sender_id})"
],
)
self.assertEqual(reminder.delivered, True)
self.assertEqual(reminder.failed, False)
assert isinstance(reminder.delivered_message_id, int)
delivered_message = Message.objects.get(id=reminder.delivered_message_id)
# The reminder message is truncated to 10,000 characters if it exceeds the limit.
assert isinstance(reminder.reminder_target_message_id, int)
length_of_reminder_content_wrapper = len(
self.get_dm_reminder_content(
"\n[message truncated]", reminder.reminder_target_message_id
)
)
self.assertEqual(
delivered_message.content,
self.get_dm_reminder_content(
content[:-length_of_reminder_content_wrapper] + "\n[message truncated]",
reminder.reminder_target_message_id,
),
)
self.assertEqual(delivered_message.date_sent, more_than_scheduled_delivery_datetime)
def test_scheduled_reminder_with_inaccessible_message(self) -> None:
# No scheduled message
result = try_deliver_one_scheduled_message()
self.assertFalse(result)
content = "Test content"
reminder = self.create_reminder(content)
# Delete the message to make it inaccessible.
assert isinstance(reminder.reminder_target_message_id, int)
Message.objects.filter(id=reminder.reminder_target_message_id).delete()
# mock current time to be greater than the scheduled time, so that the `scheduled_message` can be sent.
more_than_scheduled_delivery_datetime = reminder.scheduled_timestamp + datetime.timedelta(
minutes=1
)
with (
time_machine.travel(more_than_scheduled_delivery_datetime, tick=False),
self.assertLogs(level="INFO") as logs,
):
result = try_deliver_one_scheduled_message()
self.assertTrue(result)
reminder.refresh_from_db()
self.assertEqual(
logs.output,
[
f"INFO:root:Sending scheduled message {reminder.id} with date {reminder.scheduled_timestamp} (sender: {reminder.sender_id})"
],
)
self.assertEqual(reminder.delivered, True)
self.assertEqual(reminder.failed, False)
assert isinstance(reminder.delivered_message_id, int)
delivered_message = Message.objects.get(id=reminder.delivered_message_id)
self.assertEqual(
delivered_message.content,
self.get_dm_reminder_content(content, reminder.reminder_target_message_id),
)
self.assertEqual(delivered_message.date_sent, more_than_scheduled_delivery_datetime)

View File

@@ -3,6 +3,7 @@ import time
from datetime import timedelta
from io import StringIO
from typing import TYPE_CHECKING, Any
from unittest import mock
import orjson
import time_machine
@@ -330,7 +331,7 @@ class ScheduledMessageTest(ZulipTestCase):
self.assertEqual(message_after_deactivation.content, message_before_deactivation.content)
self.assertNotIn(expected_failure_message, message_after_deactivation.content)
def test_delivery_type_reminder_failed_to_deliver_scheduled_message_unknown_exception(
def test_failed_to_deliver_scheduled_message_unknown_exception(
self,
) -> None:
self.create_scheduled_message()
@@ -339,11 +340,14 @@ class ScheduledMessageTest(ZulipTestCase):
more_than_scheduled_delivery_datetime = scheduled_message.scheduled_timestamp + timedelta(
minutes=1
)
with time_machine.travel(more_than_scheduled_delivery_datetime, tick=False):
with (
mock.patch(
"zerver.actions.scheduled_messages.send_scheduled_message",
side_effect=Exception(),
),
time_machine.travel(more_than_scheduled_delivery_datetime, tick=False),
):
scheduled_message = self.last_scheduled_message()
scheduled_message.delivery_type = ScheduledMessage.REMIND
scheduled_message.save()
with self.assertLogs(level="INFO") as logs:
result = try_deliver_one_scheduled_message()
self.assertTrue(result)

35
zerver/views/reminders.py Normal file
View File

@@ -0,0 +1,35 @@
from django.http import HttpRequest, HttpResponse
from django.utils.timezone import now as timezone_now
from pydantic import Json
from zerver.actions.reminders import schedule_reminder_for_message
from zerver.lib.exceptions import DeliveryTimeNotInFutureError
from zerver.lib.request import RequestNotes
from zerver.lib.response import json_success
from zerver.lib.timestamp import timestamp_to_datetime
from zerver.lib.typed_endpoint import typed_endpoint
from zerver.models import UserProfile
@typed_endpoint
def create_reminders_message_backend(
request: HttpRequest,
user_profile: UserProfile,
*,
message_id: Json[int],
scheduled_delivery_timestamp: Json[int],
) -> HttpResponse:
deliver_at = timestamp_to_datetime(scheduled_delivery_timestamp)
if deliver_at <= timezone_now():
raise DeliveryTimeNotInFutureError
client = RequestNotes.get_notes(request).client
assert client is not None
reminder_id = schedule_reminder_for_message(
user_profile,
client,
message_id,
deliver_at,
)
return json_success(request, data={"reminder_id": reminder_id})

View File

@@ -150,6 +150,7 @@ from zerver.views.registration import (
realm_register,
signup_send_confirm,
)
from zerver.views.reminders import create_reminders_message_backend
from zerver.views.report import report_csp_violations
from zerver.views.saved_snippets import (
create_saved_snippet,
@@ -353,7 +354,7 @@ v1_api_and_json_patterns = [
DELETE=delete_saved_snippet,
PATCH=edit_saved_snippet,
),
# New scheduled messages are created via send_message_backend.
rest_path("reminders", POST=create_reminders_message_backend),
rest_path(
"scheduled_messages", GET=fetch_scheduled_messages, POST=create_scheduled_message_backend
),