snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
This commit is contained in:
500
zerver/actions/scheduled_messages.py
Normal file
500
zerver/actions/scheduled_messages.py
Normal file
@@ -0,0 +1,500 @@
|
||||
import logging
|
||||
from collections.abc import Sequence
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import override as override_language
|
||||
|
||||
from zerver.actions.message_send import (
|
||||
check_message,
|
||||
do_send_messages,
|
||||
internal_send_private_message,
|
||||
)
|
||||
from zerver.actions.uploads import check_attachment_reference_change, do_claim_attachments
|
||||
from zerver.lib.addressee import Addressee
|
||||
from zerver.lib.display_recipient import get_recipient_ids
|
||||
from zerver.lib.exceptions import (
|
||||
DeliveryTimeNotInFutureError,
|
||||
JsonableError,
|
||||
RealmDeactivatedError,
|
||||
UserDeactivatedError,
|
||||
)
|
||||
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, notify_remove_reminder
|
||||
from zerver.lib.scheduled_messages import access_scheduled_message
|
||||
from zerver.lib.string_validation import check_stream_topic
|
||||
from zerver.lib.timestamp import datetime_to_global_time
|
||||
from zerver.models import Client, Realm, ScheduledMessage, Subscription, UserProfile
|
||||
from zerver.models.users import get_system_bot, is_cross_realm_bot_email
|
||||
from zerver.tornado.django_api import send_event_on_commit
|
||||
|
||||
SCHEDULED_MESSAGE_LATE_CUTOFF_MINUTES = 10
|
||||
|
||||
|
||||
def check_schedule_message(
|
||||
sender: UserProfile,
|
||||
client: Client,
|
||||
recipient_type_name: str,
|
||||
message_to: list[int],
|
||||
topic_name: str | None,
|
||||
message_content: str,
|
||||
deliver_at: datetime,
|
||||
realm: Realm | None = None,
|
||||
*,
|
||||
read_by_sender: bool | None = None,
|
||||
skip_events: bool = False,
|
||||
) -> int:
|
||||
addressee = Addressee.legacy_build(sender, recipient_type_name, message_to, topic_name, realm)
|
||||
send_request = check_message(
|
||||
sender,
|
||||
client,
|
||||
addressee,
|
||||
message_content,
|
||||
realm=realm,
|
||||
)
|
||||
send_request.deliver_at = deliver_at
|
||||
|
||||
if read_by_sender is None:
|
||||
# Legacy default: a scheduled message you sent from a non-API client is
|
||||
# automatically marked as read for yourself, unless it was sent to
|
||||
# yourself only.
|
||||
read_by_sender = client.default_read_by_sender() and not addressee.is_message_to_self(
|
||||
sender
|
||||
)
|
||||
|
||||
return do_schedule_messages(
|
||||
[send_request],
|
||||
sender,
|
||||
read_by_sender=read_by_sender,
|
||||
skip_events=skip_events,
|
||||
delivery_type=ScheduledMessage.SEND_LATER,
|
||||
)[0]
|
||||
|
||||
|
||||
def notify_new_scheduled_message(
|
||||
user_profile: UserProfile, scheduled_messages: list[ScheduledMessage]
|
||||
) -> None:
|
||||
event = {
|
||||
"type": "scheduled_messages",
|
||||
"op": "add",
|
||||
"scheduled_messages": [
|
||||
scheduled_message.to_dict() for scheduled_message in scheduled_messages
|
||||
],
|
||||
}
|
||||
send_event_on_commit(user_profile.realm, event, [user_profile.id])
|
||||
|
||||
|
||||
def notify_new_reminder(user_profile: UserProfile, reminders: list[ScheduledMessage]) -> None:
|
||||
event = {
|
||||
"type": "reminders",
|
||||
"op": "add",
|
||||
"reminders": [reminder.to_reminder_dict() for reminder in reminders],
|
||||
}
|
||||
send_event_on_commit(user_profile.realm, event, [user_profile.id])
|
||||
|
||||
|
||||
def do_schedule_messages(
|
||||
send_message_requests: Sequence[SendMessageRequest],
|
||||
sender: UserProfile,
|
||||
*,
|
||||
read_by_sender: bool = False,
|
||||
skip_events: bool = False,
|
||||
delivery_type: int,
|
||||
) -> list[int]:
|
||||
scheduled_messages: list[tuple[ScheduledMessage, SendMessageRequest]] = []
|
||||
|
||||
for send_request in send_message_requests:
|
||||
scheduled_message = ScheduledMessage()
|
||||
scheduled_message.sender = send_request.message.sender
|
||||
scheduled_message.recipient = send_request.message.recipient
|
||||
topic_name = send_request.message.topic_name()
|
||||
scheduled_message.set_topic_name(topic_name=topic_name)
|
||||
scheduled_message.content = send_request.message.content
|
||||
scheduled_message.rendered_content = send_request.rendering_result.rendered_content
|
||||
scheduled_message.sending_client = send_request.message.sending_client
|
||||
scheduled_message.stream = send_request.stream
|
||||
scheduled_message.realm = send_request.realm
|
||||
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 = delivery_type
|
||||
|
||||
if delivery_type == ScheduledMessage.REMIND:
|
||||
scheduled_message.reminder_target_message_id = send_request.reminder_target_message_id
|
||||
scheduled_message.reminder_note = send_request.reminder_note
|
||||
|
||||
scheduled_messages.append((scheduled_message, send_request))
|
||||
|
||||
with transaction.atomic(durable=True):
|
||||
scheduled_message_objects = [
|
||||
scheduled_message for scheduled_message, ignored in scheduled_messages
|
||||
]
|
||||
ScheduledMessage.objects.bulk_create(scheduled_message_objects)
|
||||
for scheduled_message, send_request in scheduled_messages:
|
||||
if do_claim_attachments(
|
||||
scheduled_message, send_request.rendering_result.potential_attachment_path_ids
|
||||
):
|
||||
scheduled_message.has_attachment = True
|
||||
scheduled_message.save(update_fields=["has_attachment"])
|
||||
|
||||
if not skip_events:
|
||||
if delivery_type == ScheduledMessage.REMIND:
|
||||
notify_new_reminder(sender, scheduled_message_objects)
|
||||
else:
|
||||
notify_new_scheduled_message(sender, scheduled_message_objects)
|
||||
return [scheduled_message.id for scheduled_message, ignored in scheduled_messages]
|
||||
|
||||
|
||||
def notify_update_scheduled_message(
|
||||
user_profile: UserProfile, scheduled_message: ScheduledMessage
|
||||
) -> None:
|
||||
event = {
|
||||
"type": "scheduled_messages",
|
||||
"op": "update",
|
||||
"scheduled_message": scheduled_message.to_dict(),
|
||||
}
|
||||
send_event_on_commit(user_profile.realm, event, [user_profile.id])
|
||||
|
||||
|
||||
@transaction.atomic(durable=True)
|
||||
def edit_scheduled_message(
|
||||
sender: UserProfile,
|
||||
client: Client,
|
||||
scheduled_message_id: int,
|
||||
recipient_type_name: str | None,
|
||||
message_to: int | list[int] | None,
|
||||
topic_name: str | None,
|
||||
message_content: str | None,
|
||||
deliver_at: datetime | None,
|
||||
realm: Realm,
|
||||
) -> None:
|
||||
scheduled_message_object = access_scheduled_message(
|
||||
sender, scheduled_message_id, lock_message=True
|
||||
)
|
||||
|
||||
# We locked the row above, so a concurrent delivery has already
|
||||
# committed and this guard reliably observes its delivered=True.
|
||||
if scheduled_message_object.delivered is True:
|
||||
raise JsonableError(_("Scheduled message was already sent"))
|
||||
|
||||
# If the server failed to send the scheduled message, a new scheduled
|
||||
# delivery timestamp (`deliver_at`) is required.
|
||||
if scheduled_message_object.failed and deliver_at is None:
|
||||
raise DeliveryTimeNotInFutureError
|
||||
|
||||
# Get existing scheduled message's recipient IDs and recipient_type_name.
|
||||
existing_recipient, existing_recipient_type_name = get_recipient_ids(
|
||||
scheduled_message_object.recipient, sender.id
|
||||
)
|
||||
|
||||
send_request: SendMessageRequest | None = None
|
||||
# If any recipient information or message content has been updated,
|
||||
# we check the message again.
|
||||
if recipient_type_name is not None or message_to is not None or message_content is not None:
|
||||
# Update message type if changed.
|
||||
if recipient_type_name is not None:
|
||||
updated_recipient_type_name = recipient_type_name
|
||||
else:
|
||||
updated_recipient_type_name = existing_recipient_type_name
|
||||
|
||||
# Update message recipient if changed.
|
||||
if message_to is not None:
|
||||
if updated_recipient_type_name == "stream":
|
||||
stream_id = extract_stream_id(message_to)
|
||||
updated_recipient = [stream_id]
|
||||
else:
|
||||
updated_recipient = extract_direct_message_recipient_ids(message_to)
|
||||
else:
|
||||
updated_recipient = existing_recipient
|
||||
|
||||
# Update topic name if changed.
|
||||
if topic_name is not None:
|
||||
updated_topic_name = topic_name
|
||||
else:
|
||||
# This will be ignored in Addressee.legacy_build if type
|
||||
# is being changed from stream to direct.
|
||||
updated_topic_name = scheduled_message_object.topic_name()
|
||||
|
||||
# Update message content if changed.
|
||||
if message_content is not None:
|
||||
updated_content = message_content
|
||||
else:
|
||||
updated_content = scheduled_message_object.content
|
||||
|
||||
# Check message again.
|
||||
addressee = Addressee.legacy_build(
|
||||
sender, updated_recipient_type_name, updated_recipient, updated_topic_name
|
||||
)
|
||||
send_request = check_message(
|
||||
sender,
|
||||
client,
|
||||
addressee,
|
||||
updated_content,
|
||||
realm=realm,
|
||||
)
|
||||
|
||||
if recipient_type_name is not None or message_to is not None:
|
||||
assert send_request is not None
|
||||
# User has updated the scheduled message's recipient.
|
||||
scheduled_message_object.recipient = send_request.message.recipient
|
||||
scheduled_message_object.stream = send_request.stream
|
||||
# Update the topic based on the new recipient information.
|
||||
new_topic_name = send_request.message.topic_name()
|
||||
scheduled_message_object.set_topic_name(topic_name=new_topic_name)
|
||||
elif topic_name is not None and existing_recipient_type_name == "stream":
|
||||
# User has updated the scheduled message's topic, but not
|
||||
# the existing recipient information. We ignore topics sent
|
||||
# for scheduled direct messages.
|
||||
check_stream_topic(topic_name)
|
||||
new_topic_name = truncate_topic(topic_name)
|
||||
scheduled_message_object.set_topic_name(topic_name=new_topic_name)
|
||||
|
||||
if message_content is not None:
|
||||
assert send_request is not None
|
||||
# User has updated the scheduled messages's content.
|
||||
scheduled_message_object.content = send_request.message.content
|
||||
scheduled_message_object.rendered_content = send_request.rendering_result.rendered_content
|
||||
attachment_reference_change = check_attachment_reference_change(
|
||||
scheduled_message_object, send_request.rendering_result
|
||||
)
|
||||
scheduled_message_object.has_attachment = attachment_reference_change.did_attachment_change
|
||||
|
||||
if deliver_at is not None:
|
||||
# User has updated the scheduled message's send timestamp.
|
||||
scheduled_message_object.scheduled_timestamp = deliver_at
|
||||
|
||||
# Update for most recent Client information.
|
||||
scheduled_message_object.sending_client = client
|
||||
|
||||
# If the user is editing a scheduled message that the server tried
|
||||
# and failed to send, we need to update the `failed` boolean field
|
||||
# as well as the associated `failure_message` field.
|
||||
if scheduled_message_object.failed:
|
||||
scheduled_message_object.failed = False
|
||||
scheduled_message_object.failure_message = None
|
||||
|
||||
scheduled_message_object.save()
|
||||
|
||||
notify_update_scheduled_message(sender, scheduled_message_object)
|
||||
|
||||
|
||||
def notify_remove_scheduled_message(user_profile: UserProfile, scheduled_message_id: int) -> None:
|
||||
event = {
|
||||
"type": "scheduled_messages",
|
||||
"op": "remove",
|
||||
"scheduled_message_id": scheduled_message_id,
|
||||
}
|
||||
send_event_on_commit(user_profile.realm, event, [user_profile.id])
|
||||
|
||||
|
||||
@transaction.atomic(durable=True)
|
||||
def delete_scheduled_message(user_profile: UserProfile, scheduled_message_id: int) -> None:
|
||||
scheduled_message_object = access_scheduled_message(user_profile, scheduled_message_id)
|
||||
scheduled_message_id = scheduled_message_object.id
|
||||
scheduled_message_object.delete()
|
||||
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, scheduled_message.reminder_note
|
||||
)
|
||||
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. We use acting_user
|
||||
# to have appropriate permissions for the messages.
|
||||
message_id = internal_send_private_message(
|
||||
get_system_bot(settings.NOTIFICATION_BOT, scheduled_message.realm.id),
|
||||
current_user,
|
||||
content,
|
||||
acting_user=current_user,
|
||||
)
|
||||
if message_id is None:
|
||||
# internal_send_private_message swallows JsonableError from
|
||||
# check_message and returns None; surface that as a failure so
|
||||
# the worker marks the row failed rather than delivered with a
|
||||
# NULL delivered_message_id.
|
||||
raise JsonableError(_("Reminder could not be sent."))
|
||||
scheduled_message.delivered_message_id = message_id
|
||||
scheduled_message.delivered = True
|
||||
scheduled_message.save(update_fields=["delivered", "delivered_message_id"])
|
||||
notify_remove_reminder(current_user, scheduled_message.id)
|
||||
|
||||
|
||||
def send_scheduled_message(scheduled_message: ScheduledMessage) -> None:
|
||||
assert not scheduled_message.delivered
|
||||
assert not scheduled_message.failed
|
||||
|
||||
# Repeat the checks from validate_account_and_subdomain, in case
|
||||
# the state changed since the message was scheduled.
|
||||
if scheduled_message.realm.deactivated:
|
||||
raise RealmDeactivatedError
|
||||
|
||||
if not scheduled_message.sender.is_active:
|
||||
raise UserDeactivatedError
|
||||
|
||||
# Reminders go to the sender's own DMs from Notification Bot, so
|
||||
# the late-cutoff concern (stale messages surprising other
|
||||
# recipients) doesn't apply. Dispatch before the cutoff check so
|
||||
# reminders still fire if the worker is backed up.
|
||||
if scheduled_message.delivery_type == ScheduledMessage.REMIND:
|
||||
send_reminder(scheduled_message)
|
||||
return
|
||||
|
||||
# Limit how late we're willing to send a scheduled message.
|
||||
latest_send_time = scheduled_message.scheduled_timestamp + timedelta(
|
||||
minutes=SCHEDULED_MESSAGE_LATE_CUTOFF_MINUTES
|
||||
)
|
||||
if timezone_now() > latest_send_time:
|
||||
raise JsonableError(_("Message could not be sent at the scheduled time."))
|
||||
|
||||
# Recheck that we have permission to send this message, in case
|
||||
# permissions have changed since the message was scheduled.
|
||||
if scheduled_message.stream is not None:
|
||||
addressee = Addressee.for_stream(scheduled_message.stream, scheduled_message.topic_name())
|
||||
else:
|
||||
subscriber_ids = list(
|
||||
Subscription.objects.filter(recipient=scheduled_message.recipient).values_list(
|
||||
"user_profile_id", flat=True
|
||||
)
|
||||
)
|
||||
addressee = Addressee.for_user_ids(subscriber_ids, scheduled_message.realm)
|
||||
|
||||
# Calling check_message again is important because permissions may
|
||||
# have changed since the message was originally scheduled. This
|
||||
# means that Markdown syntax referencing mutable organization data
|
||||
# (for example, mentioning a user by name) will work (or not) as
|
||||
# if the message was sent at the delivery time, not the sending
|
||||
# time.
|
||||
send_request = check_message(
|
||||
scheduled_message.sender,
|
||||
scheduled_message.sending_client,
|
||||
addressee,
|
||||
scheduled_message.content,
|
||||
scheduled_message.realm,
|
||||
)
|
||||
|
||||
sent_message_result = do_send_messages(
|
||||
[send_request],
|
||||
mark_as_read=[scheduled_message.sender_id] if scheduled_message.read_by_sender else [],
|
||||
)[0]
|
||||
scheduled_message.delivered_message_id = sent_message_result.message_id
|
||||
scheduled_message.delivered = True
|
||||
scheduled_message.save(update_fields=["delivered", "delivered_message_id"])
|
||||
notify_remove_scheduled_message(scheduled_message.sender, scheduled_message.id)
|
||||
|
||||
|
||||
def send_failed_scheduled_message_notification(
|
||||
user_profile: UserProfile, scheduled_message_id: int
|
||||
) -> None:
|
||||
scheduled_message = access_scheduled_message(user_profile, scheduled_message_id)
|
||||
|
||||
with override_language(user_profile.default_language):
|
||||
error_string = scheduled_message.failure_message
|
||||
delivery_time_markdown = datetime_to_global_time(scheduled_message.scheduled_timestamp)
|
||||
|
||||
content = "".join(
|
||||
[
|
||||
_(
|
||||
"The message you scheduled for {delivery_datetime} was not sent because of the following error:"
|
||||
),
|
||||
"\n\n",
|
||||
"> {error_message}",
|
||||
"\n\n",
|
||||
_("[View scheduled messages](#scheduled)"),
|
||||
"\n\n",
|
||||
]
|
||||
)
|
||||
|
||||
content = content.format(
|
||||
delivery_datetime=delivery_time_markdown,
|
||||
error_message=error_string,
|
||||
)
|
||||
|
||||
internal_send_private_message(
|
||||
get_system_bot(settings.NOTIFICATION_BOT, user_profile.realm_id),
|
||||
user_profile,
|
||||
content,
|
||||
)
|
||||
|
||||
|
||||
@transaction.atomic(durable=True)
|
||||
def try_deliver_one_scheduled_message() -> bool:
|
||||
# Returns whether there was a scheduled message to attempt
|
||||
# delivery on, regardless of whether delivery succeeded.
|
||||
scheduled_message = (
|
||||
ScheduledMessage.objects.filter(
|
||||
scheduled_timestamp__lte=timezone_now(),
|
||||
delivered=False,
|
||||
failed=False,
|
||||
)
|
||||
.select_for_update(no_key=True)
|
||||
.first()
|
||||
)
|
||||
|
||||
if scheduled_message is None:
|
||||
return False
|
||||
|
||||
logging.info(
|
||||
"Sending scheduled message %s with date %s (sender: %s)",
|
||||
scheduled_message.id,
|
||||
scheduled_message.scheduled_timestamp,
|
||||
scheduled_message.sender_id,
|
||||
)
|
||||
|
||||
with override_language(scheduled_message.sender.default_language):
|
||||
try:
|
||||
send_scheduled_message(scheduled_message)
|
||||
except Exception as e:
|
||||
scheduled_message.refresh_from_db()
|
||||
was_delivered = scheduled_message.delivered
|
||||
scheduled_message.failed = True
|
||||
|
||||
if isinstance(e, JsonableError):
|
||||
scheduled_message.failure_message = e.msg
|
||||
logging.info("Failed with message: %s", e.msg)
|
||||
else:
|
||||
# An unexpected failure; store and send user a generic
|
||||
# internal server error in notification message.
|
||||
scheduled_message.failure_message = _("Internal server error")
|
||||
logging.exception(
|
||||
"Unexpected error sending scheduled message %s (sent: %s)",
|
||||
scheduled_message.id,
|
||||
was_delivered,
|
||||
stack_info=True,
|
||||
)
|
||||
|
||||
scheduled_message.save(update_fields=["failed", "failure_message"])
|
||||
|
||||
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)
|
||||
and not isinstance(e, UserDeactivatedError)
|
||||
# Cross-realm system bots (welcome-bot, notification-bot,
|
||||
# emailgateway) have no human behind them to read the
|
||||
# notification, and notifying them leaves a stray
|
||||
# notification-bot <-> system-bot DirectMessageGroup
|
||||
# attached to no realm.
|
||||
and not is_cross_realm_bot_email(scheduled_message.sender.delivery_email)
|
||||
):
|
||||
notify_update_scheduled_message(scheduled_message.sender, scheduled_message)
|
||||
send_failed_scheduled_message_notification(
|
||||
scheduled_message.sender, scheduled_message.id
|
||||
)
|
||||
|
||||
return True
|
||||
Reference in New Issue
Block a user