2016-11-14 11:12:13 -08:00
|
|
|
import datetime
|
|
|
|
import enum
|
|
|
|
import pymongo
|
|
|
|
import redis
|
|
|
|
import mongoengine as mongo
|
2016-11-17 22:13:18 -08:00
|
|
|
import boto
|
2016-11-14 11:12:13 -08:00
|
|
|
from django.conf import settings
|
|
|
|
from django.contrib.auth.models import User
|
2016-11-16 20:33:37 -08:00
|
|
|
from django.template.loader import render_to_string
|
|
|
|
from django.core.mail import EmailMultiAlternatives
|
2016-11-17 22:13:18 -08:00
|
|
|
from django.utils.html import strip_tags
|
2016-11-16 17:49:43 -08:00
|
|
|
from apps.rss_feeds.models import MStory, Feed
|
|
|
|
from apps.reader.models import UserSubscription
|
|
|
|
from apps.analyzer.models import MClassifierTitle, MClassifierAuthor, MClassifierFeed, MClassifierTag
|
|
|
|
from apps.analyzer.models import compute_story_score
|
2016-11-18 16:57:38 -08:00
|
|
|
from utils.story_functions import truncate_chars
|
2016-11-14 11:12:13 -08:00
|
|
|
from utils import log as logging
|
|
|
|
from utils import mongoengine_fields
|
2016-11-17 13:14:32 -08:00
|
|
|
from HTMLParser import HTMLParser
|
2016-11-17 19:53:02 -08:00
|
|
|
from apns import APNs, Frame, Payload
|
2016-11-14 11:12:13 -08:00
|
|
|
|
|
|
|
|
|
|
|
class NotificationFrequency(enum.Enum):
|
|
|
|
immediately = 1
|
|
|
|
hour_1 = 2
|
|
|
|
hour_6 = 3
|
|
|
|
hour_12 = 4
|
|
|
|
hour_24 = 5
|
|
|
|
|
2016-11-17 19:53:02 -08:00
|
|
|
class MUserNotificationTokens(mongo.Document):
|
|
|
|
'''A user's push notification tokens'''
|
|
|
|
user_id = mongo.IntField()
|
|
|
|
ios_tokens = mongo.ListField(mongo.StringField(max_length=1024))
|
|
|
|
|
|
|
|
meta = {
|
|
|
|
'collection': 'notification_tokens',
|
|
|
|
'indexes': [{'fields': ['user_id'],
|
|
|
|
'unique': True,
|
|
|
|
'types': False, }],
|
|
|
|
'allow_inheritance': False,
|
|
|
|
}
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def get_tokens_for_user(cls, user_id):
|
|
|
|
try:
|
|
|
|
tokens = cls.objects.get(user_id=user_id)
|
|
|
|
except cls.DoesNotExist:
|
|
|
|
tokens = cls.objects.create(user_id=user_id)
|
|
|
|
|
|
|
|
return tokens
|
2016-11-14 11:12:13 -08:00
|
|
|
|
|
|
|
class MUserFeedNotification(mongo.Document):
|
|
|
|
'''A user's notifications of a single feed.'''
|
|
|
|
user_id = mongo.IntField()
|
|
|
|
feed_id = mongo.IntField()
|
|
|
|
frequency = mongoengine_fields.IntEnumField(NotificationFrequency)
|
2016-11-15 18:18:31 -08:00
|
|
|
is_focus = mongo.BooleanField()
|
2016-11-14 11:12:13 -08:00
|
|
|
last_notification_date = mongo.DateTimeField(default=datetime.datetime.now)
|
|
|
|
is_email = mongo.BooleanField()
|
|
|
|
is_web = mongo.BooleanField()
|
|
|
|
is_ios = mongo.BooleanField()
|
|
|
|
is_android = mongo.BooleanField()
|
2016-11-17 19:13:42 -08:00
|
|
|
ios_tokens = mongo.ListField(mongo.StringField(max_length=1024))
|
|
|
|
|
2016-11-14 11:12:13 -08:00
|
|
|
|
|
|
|
meta = {
|
|
|
|
'collection': 'notifications',
|
|
|
|
'indexes': ['feed_id',
|
|
|
|
{'fields': ['user_id', 'feed_id'],
|
|
|
|
'unique': True,
|
|
|
|
'types': False, }],
|
|
|
|
'allow_inheritance': False,
|
|
|
|
}
|
|
|
|
|
|
|
|
def __unicode__(self):
|
|
|
|
notification_types = []
|
|
|
|
if self.is_email: notification_types.append('email')
|
|
|
|
if self.is_web: notification_types.append('web')
|
|
|
|
if self.is_ios: notification_types.append('ios')
|
|
|
|
if self.is_android: notification_types.append('android')
|
|
|
|
|
|
|
|
return "%s/%s: %s -> %s" % (
|
2016-11-14 19:26:09 -08:00
|
|
|
User.objects.get(pk=self.user_id).username,
|
|
|
|
Feed.get_feed_by_id(self.feed_id),
|
2016-11-14 11:12:13 -08:00
|
|
|
','.join(notification_types),
|
2016-11-14 19:26:09 -08:00
|
|
|
self.last_notification_date,
|
2016-11-14 11:12:13 -08:00
|
|
|
)
|
|
|
|
|
2016-11-16 18:29:13 -08:00
|
|
|
@classmethod
|
|
|
|
def feed_has_users(cls, feed_id):
|
|
|
|
return cls.users_for_feed(feed_id).count()
|
|
|
|
|
2016-11-15 20:45:59 -08:00
|
|
|
@classmethod
|
|
|
|
def users_for_feed(cls, feed_id):
|
|
|
|
notifications = cls.objects.filter(feed_id=feed_id)
|
|
|
|
|
|
|
|
return notifications
|
|
|
|
|
2016-11-14 19:26:09 -08:00
|
|
|
@classmethod
|
|
|
|
def feeds_for_user(cls, user_id):
|
|
|
|
notifications = cls.objects.filter(user_id=user_id)
|
|
|
|
notifications_by_feed = {}
|
|
|
|
|
|
|
|
for feed in notifications:
|
|
|
|
notifications_by_feed[feed.feed_id] = {
|
2016-11-15 18:18:31 -08:00
|
|
|
'notification_types': [],
|
|
|
|
'notification_filter': "focus" if feed.is_focus else "unread",
|
2016-11-14 19:26:09 -08:00
|
|
|
}
|
2016-11-15 18:18:31 -08:00
|
|
|
if feed.is_email: notifications_by_feed[feed.feed_id]['notification_types'].append('email')
|
|
|
|
if feed.is_web: notifications_by_feed[feed.feed_id]['notification_types'].append('web')
|
|
|
|
if feed.is_ios: notifications_by_feed[feed.feed_id]['notification_types'].append('ios')
|
|
|
|
if feed.is_android: notifications_by_feed[feed.feed_id]['notification_types'].append('android')
|
2016-11-14 19:26:09 -08:00
|
|
|
|
2016-11-15 18:18:31 -08:00
|
|
|
return notifications_by_feed
|
|
|
|
|
2016-11-15 20:45:59 -08:00
|
|
|
@classmethod
|
2016-11-16 17:49:43 -08:00
|
|
|
def push_feed_notifications(cls, feed_id, new_stories, force=False):
|
|
|
|
feed = Feed.get_by_id(feed_id)
|
|
|
|
notifications = MUserFeedNotification.users_for_feed(feed.pk)
|
2016-11-16 18:29:13 -08:00
|
|
|
logging.debug(" ---> [%-30s] ~FCPushing out notifications to ~SB%s users~SN for ~FB~SB%s stories" % (
|
2016-11-16 17:49:43 -08:00
|
|
|
feed, len(notifications), new_stories))
|
|
|
|
r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL)
|
|
|
|
|
|
|
|
latest_story_hashes = r.zrange("zF:%s" % feed.pk, -1 * new_stories, -1)
|
|
|
|
mstories = MStory.objects.filter(story_hash__in=latest_story_hashes).order_by('-story_date')
|
|
|
|
stories = Feed.format_stories(mstories)
|
|
|
|
|
2016-11-16 20:33:37 -08:00
|
|
|
for user_feed_notification in notifications:
|
2016-11-16 18:29:13 -08:00
|
|
|
sent_count = 0
|
2016-11-16 20:33:37 -08:00
|
|
|
last_notification_date = user_feed_notification.last_notification_date
|
|
|
|
try:
|
|
|
|
usersub = UserSubscription.objects.get(user=user_feed_notification.user_id,
|
|
|
|
feed=user_feed_notification.feed_id)
|
|
|
|
except UserSubscription.DoesNotExist:
|
|
|
|
continue
|
|
|
|
classifiers = user_feed_notification.classifiers(usersub)
|
2016-11-16 18:29:13 -08:00
|
|
|
|
|
|
|
if classifiers == None:
|
|
|
|
logging.debug("Has no usersubs")
|
2016-11-15 20:45:59 -08:00
|
|
|
continue
|
2016-11-16 18:29:13 -08:00
|
|
|
|
2016-11-16 17:49:43 -08:00
|
|
|
for story in stories:
|
2016-11-16 18:29:13 -08:00
|
|
|
if sent_count >= 3:
|
|
|
|
logging.debug("Sent too many, ignoring...")
|
|
|
|
continue
|
2016-11-16 17:49:43 -08:00
|
|
|
if story['story_date'] < last_notification_date and not force:
|
2016-11-16 18:29:13 -08:00
|
|
|
logging.debug("Story date older than last notification date: %s < %s" % (story['story_date'], last_notification_date))
|
2016-11-16 17:49:43 -08:00
|
|
|
continue
|
2016-11-16 18:29:13 -08:00
|
|
|
|
2016-11-16 20:33:37 -08:00
|
|
|
if story['story_date'] > user_feed_notification.last_notification_date:
|
|
|
|
user_feed_notification.last_notification_date = story['story_date']
|
|
|
|
user_feed_notification.save()
|
2016-11-16 18:29:13 -08:00
|
|
|
|
2016-11-16 20:33:37 -08:00
|
|
|
sent = user_feed_notification.push_story_notification(story, classifiers, usersub)
|
2016-11-16 18:29:13 -08:00
|
|
|
if sent: sent_count += 1
|
2016-11-16 17:49:43 -08:00
|
|
|
|
2016-11-16 20:33:37 -08:00
|
|
|
def classifiers(self, usersub):
|
2016-11-16 17:49:43 -08:00
|
|
|
classifiers = {}
|
|
|
|
if usersub.is_trained:
|
|
|
|
user = User.objects.get(pk=self.user_id)
|
|
|
|
classifiers['feeds'] = list(MClassifierFeed.objects(user_id=self.user_id, feed_id=self.feed_id,
|
|
|
|
social_user_id=0))
|
|
|
|
classifiers['authors'] = list(MClassifierAuthor.objects(user_id=self.user_id, feed_id=self.feed_id))
|
|
|
|
classifiers['titles'] = list(MClassifierTitle.objects(user_id=self.user_id, feed_id=self.feed_id))
|
|
|
|
classifiers['tags'] = list(MClassifierTag.objects(user_id=self.user_id, feed_id=self.feed_id))
|
|
|
|
|
|
|
|
return classifiers
|
|
|
|
|
2016-11-16 20:33:37 -08:00
|
|
|
def push_story_notification(self, story, classifiers, usersub):
|
2016-11-16 17:49:43 -08:00
|
|
|
story_score = self.story_score(story, classifiers)
|
|
|
|
if self.is_focus and story_score <= 0:
|
2016-11-16 18:29:13 -08:00
|
|
|
logging.debug("Is focus, but story is hidden")
|
|
|
|
return False
|
2016-11-16 17:49:43 -08:00
|
|
|
elif story_score < 0:
|
2016-11-16 18:29:13 -08:00
|
|
|
logging.debug("Is unread, but story is hidden")
|
|
|
|
return False
|
2016-11-16 17:49:43 -08:00
|
|
|
|
|
|
|
user = User.objects.get(pk=self.user_id)
|
|
|
|
logging.user(user, "~FCSending push notification: %s/%s (score: %s)" % (story['story_title'][:40], story['story_hash'], story_score))
|
|
|
|
|
2016-11-17 19:53:02 -08:00
|
|
|
self.send_web(story, user)
|
|
|
|
self.send_ios(story, user, usersub)
|
2016-11-16 17:49:43 -08:00
|
|
|
self.send_android(story)
|
2016-11-16 20:33:37 -08:00
|
|
|
self.send_email(story, usersub)
|
2016-11-16 18:29:13 -08:00
|
|
|
|
|
|
|
return True
|
2016-11-15 20:45:59 -08:00
|
|
|
|
2016-11-17 19:53:02 -08:00
|
|
|
def send_web(self, story, user):
|
2016-11-15 20:45:59 -08:00
|
|
|
if not self.is_web: return
|
2016-11-16 17:49:43 -08:00
|
|
|
|
2016-11-15 20:45:59 -08:00
|
|
|
r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL)
|
2016-11-16 17:49:43 -08:00
|
|
|
r.publish(user.username, 'notification:%s,%s' % (story['story_hash'], story['story_title']))
|
2016-11-15 20:45:59 -08:00
|
|
|
|
2016-11-17 19:53:02 -08:00
|
|
|
def send_ios(self, story, user, usersub):
|
2016-11-15 20:45:59 -08:00
|
|
|
if not self.is_ios: return
|
2016-11-17 19:53:02 -08:00
|
|
|
|
|
|
|
apns = APNs(use_sandbox=True,
|
|
|
|
cert_file='/srv/newsblur/config/certificates/aps_development.pem',
|
|
|
|
key_file='/srv/newsblur/config/certificates/aps_development.pem')
|
2016-11-15 20:45:59 -08:00
|
|
|
|
2016-11-17 19:53:02 -08:00
|
|
|
tokens = MUserNotificationTokens.get_tokens_for_user(self.user_id)
|
2016-11-17 20:11:47 -08:00
|
|
|
feed_title = usersub.user_title or usersub.feed.feed_title
|
2016-11-17 22:13:18 -08:00
|
|
|
title = "%s: %s" % (feed_title, story['story_title'])
|
2016-11-18 16:57:38 -08:00
|
|
|
body = truncate_chars(HTMLParser().unescape(strip_tags(story['story_content'])).strip(), 1600)
|
2016-11-17 20:11:47 -08:00
|
|
|
|
2016-11-17 19:53:02 -08:00
|
|
|
for token in tokens.ios_tokens:
|
|
|
|
logging.user(user, '~BMStory notification by iOS: ~FY~SB%s~SN~BM~FY/~SB%s' %
|
|
|
|
(story['story_title'][:50], usersub.feed.feed_title[:50]))
|
2016-11-17 22:13:18 -08:00
|
|
|
payload = Payload(alert={'title': title,
|
|
|
|
'body': body},
|
2016-11-18 17:37:34 -08:00
|
|
|
custom={'story_hash': story['story_hash'],
|
|
|
|
'story_feed_id': story['story_feed_id'],
|
|
|
|
})
|
2016-11-17 19:53:02 -08:00
|
|
|
apns.gateway_server.send_notification(token, payload)
|
2016-11-15 20:45:59 -08:00
|
|
|
|
|
|
|
def send_android(self, story):
|
|
|
|
if not self.is_android: return
|
|
|
|
|
|
|
|
|
2016-11-16 20:33:37 -08:00
|
|
|
def send_email(self, story, usersub):
|
2016-11-15 20:45:59 -08:00
|
|
|
if not self.is_email: return
|
2016-11-17 11:22:06 -08:00
|
|
|
feed = usersub.feed
|
2016-11-15 20:45:59 -08:00
|
|
|
|
2016-11-17 13:14:32 -08:00
|
|
|
story['story_content'] = HTMLParser().unescape(story['story_content'])
|
2016-11-16 20:33:37 -08:00
|
|
|
params = {
|
|
|
|
"story": story,
|
2016-11-17 11:22:06 -08:00
|
|
|
"feed": feed,
|
2016-11-17 16:12:17 -08:00
|
|
|
"feed_title": usersub.user_title or feed.feed_title,
|
2016-11-18 10:34:24 -08:00
|
|
|
"favicon_border": feed.favicon_color,
|
2016-11-16 20:33:37 -08:00
|
|
|
}
|
|
|
|
from_address = 'share@newsblur.com'
|
|
|
|
to_address = '%s <%s>' % (usersub.user.username, usersub.user.email)
|
|
|
|
text = render_to_string('mail/email_story_notification.txt', params)
|
|
|
|
html = render_to_string('mail/email_story_notification.xhtml', params)
|
|
|
|
subject = '%s: %s' % (usersub.user_title or usersub.feed.feed_title, story['story_title'])
|
|
|
|
subject = subject.replace('\n', ' ')
|
|
|
|
msg = EmailMultiAlternatives(subject, text,
|
|
|
|
from_email='NewsBlur <%s>' % from_address,
|
|
|
|
to=[to_address])
|
|
|
|
msg.attach_alternative(html, "text/html")
|
|
|
|
try:
|
|
|
|
msg.send()
|
|
|
|
except boto.ses.connection.ResponseError, e:
|
|
|
|
code = -1
|
|
|
|
message = "Email error: %s" % str(e)
|
|
|
|
logging.user(usersub.user, '~BMStory notification by email: ~FY~SB%s~SN~BM~FY/~SB%s' %
|
|
|
|
(story['story_title'][:50], usersub.feed.feed_title[:50]))
|
|
|
|
|
2016-11-16 17:49:43 -08:00
|
|
|
def story_score(self, story, classifiers):
|
2016-11-16 18:29:13 -08:00
|
|
|
score = compute_story_score(story, classifier_titles=classifiers.get('titles', []),
|
|
|
|
classifier_authors=classifiers.get('authors', []),
|
|
|
|
classifier_tags=classifiers.get('tags', []),
|
|
|
|
classifier_feeds=classifiers.get('feeds', []))
|
2016-11-16 17:49:43 -08:00
|
|
|
|
|
|
|
return score
|
|
|
|
|