import datetime import enum import redis import mongoengine as mongo import boto from django.conf import settings from django.contrib.auth.models import User from django.template.loader import render_to_string from django.core.mail import EmailMultiAlternatives # from django.utils.html import strip_tags 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 from utils.story_functions import truncate_chars from utils import log as logging from utils import mongoengine_fields from HTMLParser import HTMLParser from apns import APNs, Payload from BeautifulSoup import BeautifulSoup import types class NotificationFrequency(enum.Enum): immediately = 1 hour_1 = 2 hour_6 = 3 hour_12 = 4 hour_24 = 5 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 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) is_focus = mongo.BooleanField() 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() ios_tokens = mongo.ListField(mongo.StringField(max_length=1024)) 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" % ( User.objects.get(pk=self.user_id).username, Feed.get_feed_by_id(self.feed_id), ','.join(notification_types), self.last_notification_date, ) @classmethod def feed_has_users(cls, feed_id): return cls.users_for_feed(feed_id).count() @classmethod def users_for_feed(cls, feed_id): notifications = cls.objects.filter(feed_id=feed_id) return notifications @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] = { 'notification_types': [], 'notification_filter': "focus" if feed.is_focus else "unread", } 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') return notifications_by_feed @classmethod 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) logging.debug(" ---> [%-30s] ~FCPushing out notifications to ~SB%s users~SN for ~FB~SB%s stories" % ( 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) for user_feed_notification in notifications: sent_count = 0 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) if classifiers == None: logging.debug("Has no usersubs") continue for story in stories: if sent_count >= 3: logging.debug("Sent too many, ignoring...") continue if story['story_date'] < last_notification_date and not force: logging.debug("Story date older than last notification date: %s < %s" % (story['story_date'], last_notification_date)) continue if story['story_date'] > user_feed_notification.last_notification_date: user_feed_notification.last_notification_date = story['story_date'] user_feed_notification.save() story['story_content'] = HTMLParser().unescape(story['story_content']) sent = user_feed_notification.push_story_notification(story, classifiers, usersub) if sent: sent_count += 1 def classifiers(self, usersub): classifiers = {} if usersub.is_trained: 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 def title_and_body(self, story, usersub): def replace_with_newlines(element): text = '' for elem in element.recursiveChildGenerator(): if isinstance(elem, types.StringTypes): text += elem elif elem.name == 'br': text += '\n' elif elem.name == 'p': text += '\n\n' return text feed_title = usersub.user_title or usersub.feed.feed_title # title = "%s: %s" % (feed_title, story['story_title']) title = feed_title subtitle = story['story_title'] # body = HTMLParser().unescape(strip_tags(story['story_content'])) soup = BeautifulSoup(story['story_content'].strip()) # print story['story_content'] body = replace_with_newlines(soup) body = truncate_chars(body.strip(), 1600) return title, subtitle, body def push_story_notification(self, story, classifiers, usersub): story_score = self.story_score(story, classifiers) if self.is_focus and story_score <= 0: logging.debug("Is focus, but story is hidden") return False elif story_score < 0: logging.debug("Is unread, but story is hidden") return False 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)) self.send_web(story, user) self.send_ios(story, user, usersub) self.send_android(story) self.send_email(story, usersub) return True def send_web(self, story, user): if not self.is_web: return r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) r.publish(user.username, 'notification:%s,%s' % (story['story_hash'], story['story_title'])) def send_ios(self, story, user, usersub): if not self.is_ios: return apns = APNs(use_sandbox=True, cert_file='/srv/newsblur/config/certificates/aps_development.pem', key_file='/srv/newsblur/config/certificates/aps_development.pem') tokens = MUserNotificationTokens.get_tokens_for_user(self.user_id) title, subtitle, body = self.title_and_body(story, usersub) image_url = None if len(story['image_urls']): image_url = story['image_urls'][0] print image_url 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])) payload = Payload(alert={'title': title, 'subtitle': subtitle, 'body': body}, category="STORY_CATEGORY", mutable_content=True, custom={'story_hash': story['story_hash'], 'story_feed_id': story['story_feed_id'], 'image_url': image_url, }) apns.gateway_server.send_notification(token, payload) def send_android(self, story): if not self.is_android: return def send_email(self, story, usersub): if not self.is_email: return feed = usersub.feed params = { "story": story, "feed": feed, "feed_title": usersub.user_title or feed.feed_title, "favicon_border": feed.favicon_color, } 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: logging.user(usersub.user, '~BMStory notification by email error: ~FR%s' % 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])) def story_score(self, story, classifiers): 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', [])) return score