mirror of
https://github.com/zulip/zulip.git
synced 2025-10-30 11:33:51 +00:00
There are similar to what exists for scheduled messages expect the PATCH requests which will be added later when the functionality is implemented.
285 lines
11 KiB
Python
285 lines
11 KiB
Python
from typing import TypedDict
|
|
|
|
from django.conf import settings
|
|
from django.db import models
|
|
from django.db.models import CASCADE, Q
|
|
from django.utils.timezone import now as timezone_now
|
|
from typing_extensions import override
|
|
|
|
from zerver.lib.display_recipient import get_recipient_ids
|
|
from zerver.lib.timestamp import datetime_to_timestamp
|
|
from zerver.models.clients import Client
|
|
from zerver.models.constants import MAX_TOPIC_NAME_LENGTH
|
|
from zerver.models.groups import NamedUserGroup
|
|
from zerver.models.messages import Message
|
|
from zerver.models.realms import Realm
|
|
from zerver.models.recipients import Recipient
|
|
from zerver.models.streams import Stream
|
|
from zerver.models.users import UserProfile
|
|
|
|
|
|
class AbstractScheduledJob(models.Model):
|
|
scheduled_timestamp = models.DateTimeField(db_index=True)
|
|
# JSON representation of arguments to consumer
|
|
data = models.TextField()
|
|
realm = models.ForeignKey(Realm, on_delete=CASCADE)
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
|
|
class ScheduledEmail(AbstractScheduledJob):
|
|
# Exactly one of users or address should be set. These are
|
|
# duplicate values, used to efficiently filter the set of
|
|
# ScheduledEmails for use in clear_scheduled_emails; the
|
|
# recipients used for actually sending messages are stored in the
|
|
# data field of AbstractScheduledJob.
|
|
users = models.ManyToManyField(UserProfile)
|
|
# Just the address part of a full "name <address>" email address
|
|
address = models.EmailField(null=True, db_index=True)
|
|
|
|
# Valid types are below
|
|
WELCOME = 1
|
|
DIGEST = 2
|
|
INVITATION_REMINDER = 3
|
|
type = models.PositiveSmallIntegerField()
|
|
|
|
@override
|
|
def __str__(self) -> str:
|
|
return f"{self.type} {self.address or list(self.users.all())} {self.scheduled_timestamp}"
|
|
|
|
|
|
class MissedMessageEmailAddress(models.Model):
|
|
message = models.ForeignKey(Message, on_delete=CASCADE)
|
|
user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE)
|
|
email_token = models.CharField(max_length=34, unique=True, db_index=True)
|
|
|
|
# Timestamp of when the missed message address generated.
|
|
timestamp = models.DateTimeField(db_index=True, default=timezone_now)
|
|
# Number of times the missed message address has been used.
|
|
times_used = models.PositiveIntegerField(default=0, db_index=True)
|
|
|
|
@override
|
|
def __str__(self) -> str:
|
|
return settings.EMAIL_GATEWAY_PATTERN % (self.email_token,)
|
|
|
|
def increment_times_used(self) -> None:
|
|
self.times_used += 1
|
|
self.save(update_fields=["times_used"])
|
|
|
|
|
|
class NotificationTriggers:
|
|
# "direct_message" is for 1:1 and group direct messages
|
|
DIRECT_MESSAGE = "direct_message"
|
|
MENTION = "mentioned"
|
|
TOPIC_WILDCARD_MENTION = "topic_wildcard_mentioned"
|
|
STREAM_WILDCARD_MENTION = "stream_wildcard_mentioned"
|
|
STREAM_PUSH = "stream_push_notify"
|
|
STREAM_EMAIL = "stream_email_notify"
|
|
FOLLOWED_TOPIC_PUSH = "followed_topic_push_notify"
|
|
FOLLOWED_TOPIC_EMAIL = "followed_topic_email_notify"
|
|
TOPIC_WILDCARD_MENTION_IN_FOLLOWED_TOPIC = "topic_wildcard_mentioned_in_followed_topic"
|
|
STREAM_WILDCARD_MENTION_IN_FOLLOWED_TOPIC = "stream_wildcard_mentioned_in_followed_topic"
|
|
|
|
|
|
class ScheduledMessageNotificationEmail(models.Model):
|
|
"""Stores planned outgoing message notification emails. They may be
|
|
processed earlier should Zulip choose to batch multiple messages
|
|
in a single email, but typically will be processed just after
|
|
scheduled_timestamp.
|
|
"""
|
|
|
|
user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE)
|
|
message = models.ForeignKey(Message, on_delete=CASCADE)
|
|
|
|
EMAIL_NOTIFICATION_TRIGGER_CHOICES = [
|
|
(NotificationTriggers.DIRECT_MESSAGE, "Direct message"),
|
|
(NotificationTriggers.MENTION, "Mention"),
|
|
(NotificationTriggers.TOPIC_WILDCARD_MENTION, "Topic wildcard mention"),
|
|
(NotificationTriggers.STREAM_WILDCARD_MENTION, "Stream wildcard mention"),
|
|
(NotificationTriggers.STREAM_EMAIL, "Stream notifications enabled"),
|
|
(NotificationTriggers.FOLLOWED_TOPIC_EMAIL, "Followed topic notifications enabled"),
|
|
(
|
|
NotificationTriggers.TOPIC_WILDCARD_MENTION_IN_FOLLOWED_TOPIC,
|
|
"Topic wildcard mention in followed topic",
|
|
),
|
|
(
|
|
NotificationTriggers.STREAM_WILDCARD_MENTION_IN_FOLLOWED_TOPIC,
|
|
"Stream wildcard mention in followed topic",
|
|
),
|
|
]
|
|
|
|
trigger = models.TextField(choices=EMAIL_NOTIFICATION_TRIGGER_CHOICES)
|
|
mentioned_user_group = models.ForeignKey(NamedUserGroup, null=True, on_delete=CASCADE)
|
|
|
|
# Timestamp for when the notification should be processed and sent.
|
|
# Calculated from the time the event was received and the batching period.
|
|
scheduled_timestamp = models.DateTimeField(db_index=True)
|
|
|
|
|
|
class APIScheduledStreamMessageDict(TypedDict):
|
|
scheduled_message_id: int
|
|
to: int
|
|
type: str
|
|
content: str
|
|
rendered_content: str
|
|
topic: str
|
|
scheduled_delivery_timestamp: int
|
|
failed: bool
|
|
|
|
|
|
class APIScheduledDirectMessageDict(TypedDict):
|
|
scheduled_message_id: int
|
|
to: list[int]
|
|
type: str
|
|
content: str
|
|
rendered_content: str
|
|
scheduled_delivery_timestamp: int
|
|
failed: bool
|
|
|
|
|
|
class APIReminderDirectMessageDict(TypedDict):
|
|
reminder_id: int
|
|
to: list[int]
|
|
type: str
|
|
content: str
|
|
rendered_content: str
|
|
scheduled_delivery_timestamp: int
|
|
failed: bool
|
|
reminder_target_message_id: int
|
|
|
|
|
|
class ScheduledMessage(models.Model):
|
|
sender = models.ForeignKey(UserProfile, on_delete=CASCADE)
|
|
recipient = models.ForeignKey(Recipient, on_delete=CASCADE)
|
|
subject = models.CharField(max_length=MAX_TOPIC_NAME_LENGTH)
|
|
content = models.TextField()
|
|
rendered_content = models.TextField()
|
|
sending_client = models.ForeignKey(Client, on_delete=CASCADE)
|
|
stream = models.ForeignKey(Stream, null=True, on_delete=CASCADE)
|
|
realm = models.ForeignKey(Realm, on_delete=CASCADE)
|
|
scheduled_timestamp = models.DateTimeField(db_index=True)
|
|
read_by_sender = models.BooleanField()
|
|
delivered = models.BooleanField(default=False)
|
|
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.
|
|
failed = models.BooleanField(default=False)
|
|
failure_message = models.TextField(null=True)
|
|
|
|
SEND_LATER = 1
|
|
REMIND = 2
|
|
|
|
DELIVERY_TYPES = (
|
|
(SEND_LATER, "send_later"),
|
|
(REMIND, "remind"),
|
|
)
|
|
|
|
delivery_type = models.PositiveSmallIntegerField(
|
|
choices=DELIVERY_TYPES,
|
|
default=SEND_LATER,
|
|
)
|
|
|
|
class Meta:
|
|
indexes = [
|
|
# We expect a large number of delivered scheduled messages
|
|
# to accumulate over time. This first index is for the
|
|
# deliver_scheduled_messages worker.
|
|
models.Index(
|
|
name="zerver_unsent_scheduled_messages_by_time",
|
|
fields=["scheduled_timestamp"],
|
|
condition=Q(
|
|
delivered=False,
|
|
failed=False,
|
|
),
|
|
),
|
|
# This index is for displaying scheduled messages to the
|
|
# user themself via the API; we don't filter failed
|
|
# messages since we will want to display those so that
|
|
# failures don't just disappear into a black hole.
|
|
models.Index(
|
|
name="zerver_realm_unsent_scheduled_messages_by_user",
|
|
fields=["realm_id", "sender", "delivery_type", "scheduled_timestamp"],
|
|
condition=Q(
|
|
delivered=False,
|
|
),
|
|
),
|
|
]
|
|
|
|
@override
|
|
def __str__(self) -> str:
|
|
if self.recipient.type != Recipient.STREAM:
|
|
return f"{self.recipient.label()} {self.sender!r} {self.scheduled_timestamp}"
|
|
|
|
return f"{self.recipient.label()} {self.subject} {self.sender!r} {self.scheduled_timestamp}"
|
|
|
|
def topic_name(self) -> str:
|
|
return self.subject
|
|
|
|
def set_topic_name(self, topic_name: str) -> None:
|
|
self.subject = topic_name
|
|
|
|
def is_stream_message(self) -> bool:
|
|
return self.recipient.type == Recipient.STREAM
|
|
|
|
def to_dict(self) -> APIScheduledStreamMessageDict | APIScheduledDirectMessageDict:
|
|
recipient, recipient_type_str = get_recipient_ids(self.recipient, self.sender.id)
|
|
|
|
if recipient_type_str == "private":
|
|
# The topic for direct messages should always be "\x07".
|
|
assert self.topic_name() == Message.DM_TOPIC
|
|
|
|
return APIScheduledDirectMessageDict(
|
|
scheduled_message_id=self.id,
|
|
to=recipient,
|
|
type=recipient_type_str,
|
|
content=self.content,
|
|
rendered_content=self.rendered_content,
|
|
scheduled_delivery_timestamp=datetime_to_timestamp(self.scheduled_timestamp),
|
|
failed=self.failed,
|
|
)
|
|
|
|
# The recipient for stream messages should always just be the unique stream ID.
|
|
assert len(recipient) == 1
|
|
|
|
return APIScheduledStreamMessageDict(
|
|
scheduled_message_id=self.id,
|
|
to=recipient[0],
|
|
type=recipient_type_str,
|
|
content=self.content,
|
|
rendered_content=self.rendered_content,
|
|
topic=self.topic_name(),
|
|
scheduled_delivery_timestamp=datetime_to_timestamp(self.scheduled_timestamp),
|
|
failed=self.failed,
|
|
)
|
|
|
|
def to_reminder_dict(self) -> APIReminderDirectMessageDict:
|
|
assert self.reminder_target_message_id is not None
|
|
recipient, recipient_type_str = get_recipient_ids(self.recipient, self.sender.id)
|
|
assert recipient_type_str == "private"
|
|
return APIReminderDirectMessageDict(
|
|
reminder_id=self.id,
|
|
to=recipient,
|
|
type=recipient_type_str,
|
|
content=self.content,
|
|
rendered_content=self.rendered_content,
|
|
scheduled_delivery_timestamp=datetime_to_timestamp(self.scheduled_timestamp),
|
|
failed=self.failed,
|
|
reminder_target_message_id=self.reminder_target_message_id,
|
|
)
|
|
|
|
|
|
EMAIL_TYPES = {
|
|
"account_registered": ScheduledEmail.WELCOME,
|
|
"onboarding_zulip_topics": ScheduledEmail.WELCOME,
|
|
"onboarding_zulip_guide": ScheduledEmail.WELCOME,
|
|
"onboarding_team_to_zulip": ScheduledEmail.WELCOME,
|
|
"digest": ScheduledEmail.DIGEST,
|
|
"invitation_reminder": ScheduledEmail.INVITATION_REMINDER,
|
|
}
|