2013-10-21 23:26:23 +02:00
|
|
|
from __future__ import absolute_import
|
|
|
|
import datetime
|
2013-12-16 23:03:09 +01:00
|
|
|
import logging
|
2013-10-21 23:26:23 +02:00
|
|
|
|
2016-06-04 16:52:18 +02:00
|
|
|
from typing import Any
|
|
|
|
|
2013-12-16 23:03:09 +01:00
|
|
|
from django.conf import settings
|
2013-10-21 23:26:23 +02:00
|
|
|
from django.core.management.base import BaseCommand
|
2017-02-25 21:50:51 +01:00
|
|
|
from django.utils import timezone
|
2013-10-21 23:26:23 +02:00
|
|
|
|
|
|
|
from zerver.lib.queue import queue_json_publish
|
2017-01-04 09:20:23 +01:00
|
|
|
from zerver.models import UserActivity, UserProfile, Realm
|
2013-10-21 23:26:23 +02:00
|
|
|
|
2013-12-16 23:03:09 +01:00
|
|
|
## Logging setup ##
|
|
|
|
|
|
|
|
log_format = "%(asctime)s: %(message)s"
|
|
|
|
logging.basicConfig(format=log_format)
|
|
|
|
|
|
|
|
formatter = logging.Formatter(log_format)
|
|
|
|
file_handler = logging.FileHandler(settings.DIGEST_LOG_PATH)
|
|
|
|
file_handler.setFormatter(formatter)
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
logger.setLevel(logging.DEBUG)
|
|
|
|
logger.addHandler(file_handler)
|
|
|
|
|
|
|
|
|
2014-01-06 22:05:44 +01:00
|
|
|
VALID_DIGEST_DAYS = (1, 2, 3, 4)
|
2013-10-21 23:26:23 +02:00
|
|
|
def inactive_since(user_profile, cutoff):
|
2016-06-04 16:52:18 +02:00
|
|
|
# type: (UserProfile, datetime.datetime) -> bool
|
2013-10-21 23:26:23 +02:00
|
|
|
# Hasn't used the app in the last 24 business-day hours.
|
2016-12-02 08:15:16 +01:00
|
|
|
most_recent_visit = [row.last_visit for row in
|
2016-11-30 14:17:35 +01:00
|
|
|
UserActivity.objects.filter(
|
2017-01-24 07:06:13 +01:00
|
|
|
user_profile=user_profile)]
|
2013-10-21 23:26:23 +02:00
|
|
|
|
|
|
|
if not most_recent_visit:
|
|
|
|
# This person has never used the app.
|
|
|
|
return True
|
|
|
|
|
|
|
|
last_visit = max(most_recent_visit)
|
|
|
|
return last_visit < cutoff
|
|
|
|
|
|
|
|
def last_business_day():
|
2016-06-04 16:52:18 +02:00
|
|
|
# type: () -> datetime.datetime
|
2013-12-16 23:30:21 +01:00
|
|
|
one_day = datetime.timedelta(hours=23)
|
2017-02-25 21:50:51 +01:00
|
|
|
previous_day = timezone.now() - one_day
|
2013-10-21 23:26:23 +02:00
|
|
|
while previous_day.weekday() not in VALID_DIGEST_DAYS:
|
|
|
|
previous_day -= one_day
|
|
|
|
return previous_day
|
|
|
|
|
|
|
|
# Changes to this should also be reflected in
|
|
|
|
# zerver/worker/queue_processors.py:DigestWorker.consume()
|
|
|
|
def queue_digest_recipient(user_profile, cutoff):
|
2016-06-04 16:52:18 +02:00
|
|
|
# type: (UserProfile, datetime.datetime) -> None
|
2013-10-21 23:26:23 +02:00
|
|
|
# Convert cutoff to epoch seconds for transit.
|
|
|
|
event = {"user_profile_id": user_profile.id,
|
|
|
|
"cutoff": cutoff.strftime('%s')}
|
|
|
|
queue_json_publish("digest_emails", event, lambda event: None)
|
|
|
|
|
2017-01-04 09:20:23 +01:00
|
|
|
def realms_for_this_deployment():
|
2016-06-04 16:52:18 +02:00
|
|
|
# type: () -> List[str]
|
2016-07-19 06:38:23 +02:00
|
|
|
if settings.ZILENCER_ENABLED:
|
2015-08-21 11:24:18 +02:00
|
|
|
# Voyager deployments don't have a Deployment entry.
|
2013-12-19 19:02:41 +01:00
|
|
|
# Only send zulip.com digests on staging.
|
|
|
|
from zilencer.models import Deployment
|
|
|
|
site_url = settings.EXTERNAL_URI_SCHEME + settings.EXTERNAL_HOST.rstrip("/")
|
|
|
|
try:
|
|
|
|
deployment = Deployment.objects.select_related('realms').get(
|
|
|
|
base_site_url__startswith=site_url)
|
|
|
|
except Deployment.DoesNotExist:
|
|
|
|
raise ValueError("digest: Unable to determine deployment.")
|
|
|
|
|
2017-01-04 09:20:23 +01:00
|
|
|
return [r.string_id for r in deployment.realms.all()]
|
2015-08-21 11:24:18 +02:00
|
|
|
# Voyager and development.
|
2013-12-31 22:34:52 +01:00
|
|
|
return []
|
2013-12-19 19:02:41 +01:00
|
|
|
|
2017-01-04 09:20:23 +01:00
|
|
|
def should_process_digest(realm_str, deployment_realms):
|
2016-06-04 16:52:18 +02:00
|
|
|
# type: (str, List[str]) -> bool
|
2017-01-04 09:20:23 +01:00
|
|
|
if realm_str in settings.SYSTEM_ONLY_REALMS:
|
2016-08-12 20:24:04 +02:00
|
|
|
# Don't try to send emails to system-only realms
|
|
|
|
return False
|
|
|
|
if settings.PRODUCTION and not settings.VOYAGER:
|
2015-08-21 11:24:18 +02:00
|
|
|
# zulip.com or staging.zulip.com
|
2017-01-04 09:20:23 +01:00
|
|
|
return realm_str in deployment_realms
|
2016-08-12 20:24:04 +02:00
|
|
|
return True
|
2013-12-19 19:02:41 +01:00
|
|
|
|
2013-10-21 23:26:23 +02:00
|
|
|
class Command(BaseCommand):
|
|
|
|
help = """Enqueue digest emails for users that haven't checked the app
|
|
|
|
in a while.
|
|
|
|
"""
|
2016-11-29 07:22:02 +01:00
|
|
|
|
2013-10-21 23:26:23 +02:00
|
|
|
def handle(self, *args, **options):
|
2016-06-04 16:52:18 +02:00
|
|
|
# type: (*Any, **Any) -> None
|
2013-10-21 23:26:23 +02:00
|
|
|
# To be really conservative while we don't have user timezones or
|
|
|
|
# special-casing for companies with non-standard workweeks, only
|
|
|
|
# try to send mail on Tuesdays, Wednesdays, and Thursdays.
|
|
|
|
if datetime.datetime.utcnow().weekday() not in VALID_DIGEST_DAYS:
|
|
|
|
return
|
|
|
|
|
2017-01-04 09:20:23 +01:00
|
|
|
deployment_realms = realms_for_this_deployment()
|
2015-11-01 17:10:46 +01:00
|
|
|
for realm in Realm.objects.filter(deactivated=False, show_digest_email=True):
|
2017-01-04 09:20:23 +01:00
|
|
|
if not should_process_digest(realm.string_id, deployment_realms):
|
2013-12-19 19:02:41 +01:00
|
|
|
continue
|
|
|
|
|
2013-12-16 22:54:01 +01:00
|
|
|
user_profiles = UserProfile.objects.filter(
|
2017-01-04 09:20:23 +01:00
|
|
|
realm=realm, is_active=True, is_bot=False, enable_digest_emails=True)
|
2013-12-16 22:54:01 +01:00
|
|
|
|
|
|
|
for user_profile in user_profiles:
|
|
|
|
cutoff = last_business_day()
|
|
|
|
if inactive_since(user_profile, cutoff):
|
|
|
|
queue_digest_recipient(user_profile, cutoff)
|
2014-01-08 17:33:12 +01:00
|
|
|
logger.info("%s is inactive, queuing for potential digest" % (
|
2017-01-24 07:06:13 +01:00
|
|
|
user_profile.email,))
|