mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			253 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			253 lines
		
	
	
		
			9.3 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 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)
 | 
						|
 | 
						|
    # 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:
 | 
						|
        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 an empty string.
 | 
						|
            assert self.topic_name() == ""
 | 
						|
 | 
						|
            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,
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
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,
 | 
						|
}
 |