mirror of https://github.com/zulip/zulip.git
models: Extract zerver.models.scheduled_jobs.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
parent
73e68050bb
commit
c9c819e1d7
|
@ -68,7 +68,6 @@ from zerver.models import (
|
||||||
Client,
|
Client,
|
||||||
Huddle,
|
Huddle,
|
||||||
Message,
|
Message,
|
||||||
NotificationTriggers,
|
|
||||||
PreregistrationUser,
|
PreregistrationUser,
|
||||||
Realm,
|
Realm,
|
||||||
RealmAuditLog,
|
RealmAuditLog,
|
||||||
|
@ -80,6 +79,7 @@ from zerver.models import (
|
||||||
)
|
)
|
||||||
from zerver.models.clients import get_client
|
from zerver.models.clients import get_client
|
||||||
from zerver.models.groups import SystemGroups
|
from zerver.models.groups import SystemGroups
|
||||||
|
from zerver.models.scheduled_jobs import NotificationTriggers
|
||||||
from zerver.models.users import get_user, is_cross_realm_bot_email
|
from zerver.models.users import get_user, is_cross_realm_bot_email
|
||||||
from zilencer.models import (
|
from zilencer.models import (
|
||||||
RemoteInstallationCount,
|
RemoteInstallationCount,
|
||||||
|
|
|
@ -94,7 +94,6 @@ from zerver.lib.widget import do_widget_post_save_actions
|
||||||
from zerver.models import (
|
from zerver.models import (
|
||||||
Client,
|
Client,
|
||||||
Message,
|
Message,
|
||||||
NotificationTriggers,
|
|
||||||
Realm,
|
Realm,
|
||||||
Recipient,
|
Recipient,
|
||||||
Stream,
|
Stream,
|
||||||
|
@ -107,6 +106,7 @@ from zerver.models import (
|
||||||
from zerver.models.clients import get_client
|
from zerver.models.clients import get_client
|
||||||
from zerver.models.groups import SystemGroups
|
from zerver.models.groups import SystemGroups
|
||||||
from zerver.models.recipients import get_huddle_user_ids
|
from zerver.models.recipients import get_huddle_user_ids
|
||||||
|
from zerver.models.scheduled_jobs import NotificationTriggers
|
||||||
from zerver.models.streams import get_stream, get_stream_by_id_in_realm
|
from zerver.models.streams import get_stream, get_stream_by_id_in_realm
|
||||||
from zerver.models.users import get_system_bot, get_user_by_delivery_email, is_cross_realm_bot_email
|
from zerver.models.users import get_system_bot, get_user_by_delivery_email, is_cross_realm_bot_email
|
||||||
from zerver.tornado.django_api import send_event
|
from zerver.tornado.django_api import send_event
|
||||||
|
|
|
@ -36,16 +36,9 @@ from zerver.lib.url_encoding import (
|
||||||
stream_narrow_url,
|
stream_narrow_url,
|
||||||
topic_narrow_url,
|
topic_narrow_url,
|
||||||
)
|
)
|
||||||
from zerver.models import (
|
from zerver.models import Message, Realm, Recipient, Stream, UserMessage, UserProfile
|
||||||
Message,
|
|
||||||
NotificationTriggers,
|
|
||||||
Realm,
|
|
||||||
Recipient,
|
|
||||||
Stream,
|
|
||||||
UserMessage,
|
|
||||||
UserProfile,
|
|
||||||
)
|
|
||||||
from zerver.models.messages import get_context_for_message
|
from zerver.models.messages import get_context_for_message
|
||||||
|
from zerver.models.scheduled_jobs import NotificationTriggers
|
||||||
from zerver.models.users import get_user_profile_by_id
|
from zerver.models.users import get_user_profile_by_id
|
||||||
|
|
||||||
if sys.version_info < (3, 9): # nocoverage
|
if sys.version_info < (3, 9): # nocoverage
|
||||||
|
|
|
@ -4,7 +4,8 @@ from typing import Any, Collection, Dict, List, Optional, Set
|
||||||
|
|
||||||
from zerver.lib.mention import MentionData
|
from zerver.lib.mention import MentionData
|
||||||
from zerver.lib.user_groups import get_user_group_direct_member_ids
|
from zerver.lib.user_groups import get_user_group_direct_member_ids
|
||||||
from zerver.models import NotificationTriggers, UserGroup, UserProfile, UserTopic
|
from zerver.models import UserGroup, UserProfile, UserTopic
|
||||||
|
from zerver.models.scheduled_jobs import NotificationTriggers
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
|
@ -57,7 +57,6 @@ from zerver.models import (
|
||||||
AbstractPushDeviceToken,
|
AbstractPushDeviceToken,
|
||||||
ArchivedMessage,
|
ArchivedMessage,
|
||||||
Message,
|
Message,
|
||||||
NotificationTriggers,
|
|
||||||
PushDeviceToken,
|
PushDeviceToken,
|
||||||
Realm,
|
Realm,
|
||||||
Recipient,
|
Recipient,
|
||||||
|
@ -67,6 +66,7 @@ from zerver.models import (
|
||||||
UserProfile,
|
UserProfile,
|
||||||
)
|
)
|
||||||
from zerver.models.realms import get_fake_email_domain
|
from zerver.models.realms import get_fake_email_domain
|
||||||
|
from zerver.models.scheduled_jobs import NotificationTriggers
|
||||||
from zerver.models.users import get_user_profile_by_id
|
from zerver.models.users import get_user_profile_by_id
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
|
@ -3,11 +3,10 @@ from typing import List, Union
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from zerver.lib.exceptions import ResourceNotFoundError
|
from zerver.lib.exceptions import ResourceNotFoundError
|
||||||
from zerver.models import (
|
from zerver.models import ScheduledMessage, UserProfile
|
||||||
|
from zerver.models.scheduled_jobs import (
|
||||||
APIScheduledDirectMessageDict,
|
APIScheduledDirectMessageDict,
|
||||||
APIScheduledStreamMessageDict,
|
APIScheduledStreamMessageDict,
|
||||||
ScheduledMessage,
|
|
||||||
UserProfile,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,8 @@ from django.utils.translation import override as override_language
|
||||||
|
|
||||||
from confirmation.models import generate_key
|
from confirmation.models import generate_key
|
||||||
from zerver.lib.logging_util import log_to_file
|
from zerver.lib.logging_util import log_to_file
|
||||||
from zerver.models import EMAIL_TYPES, Realm, ScheduledEmail, UserProfile
|
from zerver.models import Realm, ScheduledEmail, UserProfile
|
||||||
|
from zerver.models.scheduled_jobs import EMAIL_TYPES
|
||||||
from zerver.models.users import get_user_profile_by_id
|
from zerver.models.users import get_user_profile_by_id
|
||||||
from zproject.email_backends import EmailLogBackEnd, get_forward_address
|
from zproject.email_backends import EmailLogBackEnd, get_forward_address
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,6 @@ from zerver.lib.queue import queue_json_publish
|
||||||
from zerver.lib.utils import assert_is_not_none
|
from zerver.lib.utils import assert_is_not_none
|
||||||
from zerver.models import (
|
from zerver.models import (
|
||||||
Message,
|
Message,
|
||||||
NotificationTriggers,
|
|
||||||
Realm,
|
Realm,
|
||||||
RealmAuditLog,
|
RealmAuditLog,
|
||||||
Recipient,
|
Recipient,
|
||||||
|
@ -23,6 +22,7 @@ from zerver.models import (
|
||||||
UserMessage,
|
UserMessage,
|
||||||
UserProfile,
|
UserProfile,
|
||||||
)
|
)
|
||||||
|
from zerver.models.scheduled_jobs import NotificationTriggers
|
||||||
|
|
||||||
logger = logging.getLogger("zulip.soft_deactivation")
|
logger = logging.getLogger("zulip.soft_deactivation")
|
||||||
log_to_file(logger, settings.SOFT_DEACTIVATION_LOG_PATH)
|
log_to_file(logger, settings.SOFT_DEACTIVATION_LOG_PATH)
|
||||||
|
|
|
@ -6,7 +6,7 @@ from django.db.migrations.state import StateApps
|
||||||
from django.db.models import F, Func, JSONField, TextField, Value
|
from django.db.models import F, Func, JSONField, TextField, Value
|
||||||
from django.db.models.functions import Cast
|
from django.db.models.functions import Cast
|
||||||
|
|
||||||
# ScheduledMessage.type for onboarding emails from zerver/models/__init__.py
|
# ScheduledMessage.type for onboarding emails from zerver/models/scheduled_jobs.py
|
||||||
WELCOME = 1
|
WELCOME = 1
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
# https://github.com/typeddjango/django-stubs/issues/1698
|
# https://github.com/typeddjango/django-stubs/issues/1698
|
||||||
# mypy: disable-error-code="explicit-override"
|
# mypy: disable-error-code="explicit-override"
|
||||||
|
|
||||||
from typing import Any, Callable, Dict, List, Tuple, TypedDict, TypeVar, Union
|
from typing import Any, Callable, Dict, List, Tuple, TypeVar, Union
|
||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -23,8 +22,6 @@ from zerver.lib.cache import (
|
||||||
realm_alert_words_automaton_cache_key,
|
realm_alert_words_automaton_cache_key,
|
||||||
realm_alert_words_cache_key,
|
realm_alert_words_cache_key,
|
||||||
)
|
)
|
||||||
from zerver.lib.display_recipient import get_recipient_ids
|
|
||||||
from zerver.lib.timestamp import datetime_to_timestamp
|
|
||||||
from zerver.lib.types import (
|
from zerver.lib.types import (
|
||||||
ExtendedFieldElement,
|
ExtendedFieldElement,
|
||||||
ExtendedValidator,
|
ExtendedValidator,
|
||||||
|
@ -45,7 +42,6 @@ from zerver.lib.validator import (
|
||||||
validate_select_field,
|
validate_select_field,
|
||||||
)
|
)
|
||||||
from zerver.models.clients import Client as Client
|
from zerver.models.clients import Client as Client
|
||||||
from zerver.models.constants import MAX_TOPIC_NAME_LENGTH
|
|
||||||
from zerver.models.drafts import Draft as Draft
|
from zerver.models.drafts import Draft as Draft
|
||||||
from zerver.models.groups import GroupGroupMembership as GroupGroupMembership
|
from zerver.models.groups import GroupGroupMembership as GroupGroupMembership
|
||||||
from zerver.models.groups import UserGroup as UserGroup
|
from zerver.models.groups import UserGroup as UserGroup
|
||||||
|
@ -85,6 +81,13 @@ from zerver.models.realms import RealmAuthenticationMethod as RealmAuthenticatio
|
||||||
from zerver.models.realms import RealmDomain as RealmDomain
|
from zerver.models.realms import RealmDomain as RealmDomain
|
||||||
from zerver.models.recipients import Huddle as Huddle
|
from zerver.models.recipients import Huddle as Huddle
|
||||||
from zerver.models.recipients import Recipient as Recipient
|
from zerver.models.recipients import Recipient as Recipient
|
||||||
|
from zerver.models.scheduled_jobs import AbstractScheduledJob as AbstractScheduledJob
|
||||||
|
from zerver.models.scheduled_jobs import MissedMessageEmailAddress as MissedMessageEmailAddress
|
||||||
|
from zerver.models.scheduled_jobs import ScheduledEmail as ScheduledEmail
|
||||||
|
from zerver.models.scheduled_jobs import ScheduledMessage as ScheduledMessage
|
||||||
|
from zerver.models.scheduled_jobs import (
|
||||||
|
ScheduledMessageNotificationEmail as ScheduledMessageNotificationEmail,
|
||||||
|
)
|
||||||
from zerver.models.streams import DefaultStream as DefaultStream
|
from zerver.models.streams import DefaultStream as DefaultStream
|
||||||
from zerver.models.streams import DefaultStreamGroup as DefaultStreamGroup
|
from zerver.models.streams import DefaultStreamGroup as DefaultStreamGroup
|
||||||
from zerver.models.streams import Stream as Stream
|
from zerver.models.streams import Stream as Stream
|
||||||
|
@ -151,240 +154,6 @@ def query_for_ids(
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
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 direct messages as well as huddles
|
|
||||||
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(UserGroup, 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) -> Union[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,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class AbstractRealmAuditLog(models.Model):
|
class AbstractRealmAuditLog(models.Model):
|
||||||
"""Defines fields common to RealmAuditLog and RemoteRealmAuditLog."""
|
"""Defines fields common to RealmAuditLog and RemoteRealmAuditLog."""
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,255 @@
|
||||||
|
# https://github.com/typeddjango/django-stubs/issues/1698
|
||||||
|
# mypy: disable-error-code="explicit-override"
|
||||||
|
|
||||||
|
from typing import List, TypedDict, Union
|
||||||
|
|
||||||
|
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 UserGroup
|
||||||
|
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 direct messages as well as huddles
|
||||||
|
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(UserGroup, 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) -> Union[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,
|
||||||
|
}
|
|
@ -8,7 +8,8 @@ from zerver.actions.user_topics import do_set_user_topic_visibility_policy
|
||||||
from zerver.lib.push_notifications import get_apns_badge_count, get_apns_badge_count_future
|
from zerver.lib.push_notifications import get_apns_badge_count, get_apns_badge_count_future
|
||||||
from zerver.lib.test_classes import ZulipTestCase
|
from zerver.lib.test_classes import ZulipTestCase
|
||||||
from zerver.lib.test_helpers import mock_queue_publish
|
from zerver.lib.test_helpers import mock_queue_publish
|
||||||
from zerver.models import NotificationTriggers, Subscription, UserPresence, UserTopic
|
from zerver.models import Subscription, UserPresence, UserTopic
|
||||||
|
from zerver.models.scheduled_jobs import NotificationTriggers
|
||||||
from zerver.models.streams import get_stream
|
from zerver.models.streams import get_stream
|
||||||
from zerver.tornado.event_queue import maybe_enqueue_notifications
|
from zerver.tornado.event_queue import maybe_enqueue_notifications
|
||||||
|
|
||||||
|
|
|
@ -27,9 +27,10 @@ from zerver.lib.email_notifications import (
|
||||||
)
|
)
|
||||||
from zerver.lib.send_email import FromAddress
|
from zerver.lib.send_email import FromAddress
|
||||||
from zerver.lib.test_classes import ZulipTestCase
|
from zerver.lib.test_classes import ZulipTestCase
|
||||||
from zerver.models import NotificationTriggers, UserMessage, UserProfile, UserTopic
|
from zerver.models import UserMessage, UserProfile, UserTopic
|
||||||
from zerver.models.realm_emoji import get_name_keyed_dict_for_active_realm_emoji
|
from zerver.models.realm_emoji import get_name_keyed_dict_for_active_realm_emoji
|
||||||
from zerver.models.realms import get_realm
|
from zerver.models.realms import get_realm
|
||||||
|
from zerver.models.scheduled_jobs import NotificationTriggers
|
||||||
from zerver.models.streams import get_stream
|
from zerver.models.streams import get_stream
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ from zerver.actions.user_groups import check_add_user_group
|
||||||
from zerver.lib.mention import MentionBackend, MentionData
|
from zerver.lib.mention import MentionBackend, MentionData
|
||||||
from zerver.lib.notification_data import UserMessageNotificationsData, get_user_group_mentions_data
|
from zerver.lib.notification_data import UserMessageNotificationsData, get_user_group_mentions_data
|
||||||
from zerver.lib.test_classes import ZulipTestCase
|
from zerver.lib.test_classes import ZulipTestCase
|
||||||
from zerver.models import NotificationTriggers
|
from zerver.models.scheduled_jobs import NotificationTriggers
|
||||||
|
|
||||||
|
|
||||||
class TestNotificationData(ZulipTestCase):
|
class TestNotificationData(ZulipTestCase):
|
||||||
|
|
|
@ -12,8 +12,9 @@ from zerver.lib.outgoing_webhook import get_service_interface_class, process_suc
|
||||||
from zerver.lib.test_classes import ZulipTestCase
|
from zerver.lib.test_classes import ZulipTestCase
|
||||||
from zerver.lib.timestamp import datetime_to_timestamp
|
from zerver.lib.timestamp import datetime_to_timestamp
|
||||||
from zerver.lib.topic import TOPIC_NAME
|
from zerver.lib.topic import TOPIC_NAME
|
||||||
from zerver.models import SLACK_INTERFACE, Message, NotificationTriggers
|
from zerver.models import SLACK_INTERFACE, Message
|
||||||
from zerver.models.realms import get_realm
|
from zerver.models.realms import get_realm
|
||||||
|
from zerver.models.scheduled_jobs import NotificationTriggers
|
||||||
from zerver.models.streams import get_stream
|
from zerver.models.streams import get_stream
|
||||||
from zerver.models.users import get_user
|
from zerver.models.users import get_user
|
||||||
from zerver.openapi.openapi import validate_against_openapi_schema
|
from zerver.openapi.openapi import validate_against_openapi_schema
|
||||||
|
|
|
@ -80,7 +80,6 @@ from zerver.lib.timestamp import datetime_to_timestamp
|
||||||
from zerver.lib.user_counts import realm_user_count_by_role
|
from zerver.lib.user_counts import realm_user_count_by_role
|
||||||
from zerver.models import (
|
from zerver.models import (
|
||||||
Message,
|
Message,
|
||||||
NotificationTriggers,
|
|
||||||
PushDeviceToken,
|
PushDeviceToken,
|
||||||
Realm,
|
Realm,
|
||||||
RealmAuditLog,
|
RealmAuditLog,
|
||||||
|
@ -93,6 +92,7 @@ from zerver.models import (
|
||||||
)
|
)
|
||||||
from zerver.models.clients import get_client
|
from zerver.models.clients import get_client
|
||||||
from zerver.models.realms import get_realm
|
from zerver.models.realms import get_realm
|
||||||
|
from zerver.models.scheduled_jobs import NotificationTriggers
|
||||||
from zerver.models.streams import get_stream
|
from zerver.models.streams import get_stream
|
||||||
from zilencer.models import RemoteZulipServerAuditLog
|
from zilencer.models import RemoteZulipServerAuditLog
|
||||||
from zilencer.views import DevicesToCleanUpDict
|
from zilencer.views import DevicesToCleanUpDict
|
||||||
|
|
|
@ -26,7 +26,6 @@ from zerver.lib.send_email import EmailNotDeliveredError, FromAddress
|
||||||
from zerver.lib.test_classes import ZulipTestCase
|
from zerver.lib.test_classes import ZulipTestCase
|
||||||
from zerver.lib.test_helpers import mock_queue_publish
|
from zerver.lib.test_helpers import mock_queue_publish
|
||||||
from zerver.models import (
|
from zerver.models import (
|
||||||
NotificationTriggers,
|
|
||||||
PreregistrationUser,
|
PreregistrationUser,
|
||||||
ScheduledMessageNotificationEmail,
|
ScheduledMessageNotificationEmail,
|
||||||
UserActivity,
|
UserActivity,
|
||||||
|
@ -34,6 +33,7 @@ from zerver.models import (
|
||||||
)
|
)
|
||||||
from zerver.models.clients import get_client
|
from zerver.models.clients import get_client
|
||||||
from zerver.models.realms import get_realm
|
from zerver.models.realms import get_realm
|
||||||
|
from zerver.models.scheduled_jobs import NotificationTriggers
|
||||||
from zerver.models.streams import get_stream
|
from zerver.models.streams import get_stream
|
||||||
from zerver.tornado.event_queue import build_offline_notification
|
from zerver.tornado.event_queue import build_offline_notification
|
||||||
from zerver.worker import queue_processors
|
from zerver.worker import queue_processors
|
||||||
|
|
|
@ -15,8 +15,9 @@ from zerver.lib.bot_storage import StateError
|
||||||
from zerver.lib.test_classes import ZulipTestCase
|
from zerver.lib.test_classes import ZulipTestCase
|
||||||
from zerver.lib.test_helpers import mock_queue_publish
|
from zerver.lib.test_helpers import mock_queue_publish
|
||||||
from zerver.lib.validator import check_string
|
from zerver.lib.validator import check_string
|
||||||
from zerver.models import NotificationTriggers, Recipient, UserProfile
|
from zerver.models import Recipient, UserProfile
|
||||||
from zerver.models.realms import get_realm
|
from zerver.models.realms import get_realm
|
||||||
|
from zerver.models.scheduled_jobs import NotificationTriggers
|
||||||
|
|
||||||
BOT_TYPE_TO_QUEUE_NAME = {
|
BOT_TYPE_TO_QUEUE_NAME = {
|
||||||
UserProfile.OUTGOING_WEBHOOK_BOT: "outgoing_webhooks",
|
UserProfile.OUTGOING_WEBHOOK_BOT: "outgoing_webhooks",
|
||||||
|
|
|
@ -11,12 +11,8 @@ from zerver.lib.initial_password import initial_password
|
||||||
from zerver.lib.test_classes import ZulipTestCase
|
from zerver.lib.test_classes import ZulipTestCase
|
||||||
from zerver.lib.test_helpers import get_test_image_file, ratelimit_rule
|
from zerver.lib.test_helpers import get_test_image_file, ratelimit_rule
|
||||||
from zerver.lib.users import get_all_api_keys
|
from zerver.lib.users import get_all_api_keys
|
||||||
from zerver.models import (
|
from zerver.models import Draft, ScheduledMessageNotificationEmail, UserProfile
|
||||||
Draft,
|
from zerver.models.scheduled_jobs import NotificationTriggers
|
||||||
NotificationTriggers,
|
|
||||||
ScheduledMessageNotificationEmail,
|
|
||||||
UserProfile,
|
|
||||||
)
|
|
||||||
from zerver.models.users import get_user_profile_by_api_key
|
from zerver.models.users import get_user_profile_by_api_key
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
Loading…
Reference in New Issue