mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-31 12:03:46 +00:00 
			
		
		
		
	This avoids a potential unnecessary message.recipient fetch required by is_stream_message(). is_stream_message() methods precedes the addition of the denormalized is_channel_message column and is now unnecessary. In practice, we usually fetch Message objects with `.recipient` already, so I don't expect any notable performance impact here - but it's still a useful change to make.
		
			
				
	
	
		
			286 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			286 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)
 | |
|     reminder_note = models.TextField(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_channel_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,
 | |
| }
 |