def user_logged_in(shard, user_id): """Logs in a user to a shard. Always returns the current user ID.""" login_record = None if user_id: # Re-login the user if they somehow lost their browser state and # needed to reload the page. This assumes the cookie was okay. login_record = models.LoginRecord.get_by_id(user_id) if login_record and not login_record.online: def txn(): login_record = models.LoginRecord.get_by_id(user_id) assert login_record login_record.online = True login_record.put() logging.debug('Re-logging-in user_id=%r to shard=%r', login_record.user_id, shard) ndb.transaction(txn) # User is logging in for the first time or somehow state was deleted. if not login_record: login_record = models.LoginRecord( key=ndb.Key(models.LoginRecord._get_kind(), models.human_uuid()), shard_id=shard, online=True) login_record.put() logging.debug('Logged-in new user_id=%r to shard=%r', login_record.user_id, shard) invalidate_user_cache(shard) return login_record.user_id
def txn(): shard_id = models.human_uuid() shard = models.Shard.get_by_id( shard_id, use_cache=False, use_memcache=False) if shard: raise ndb.Rollback() shard = models.Shard(id=shard_id) shard.put() return shard_id
def insert_post(shard, **kwargs): """Inserts a post at the present time, returning its key. If the post_id keyword argument is not supplied, a new post ID will be auto assigned. """ # Create the posting and insert it. post_id = kwargs.pop('post_id', None) if not post_id: post_id = models.human_uuid() new_topic = kwargs.get('new_topic', None) kwargs['post_time'] = datetime.datetime.now() post_key = ndb.Key(models.Post._get_kind(), post_id) post = models.Post( key=post_key, **kwargs) @ndb.tasklet def txn(): if (yield post_key.get_async(use_memcache=False, use_cache=False)): logging.warning('Post already exists for shard=%r, post_id=%r', shard, post_id) raise ndb.Rollback() yield post.put_async(use_memcache=False, use_cache=False) # Pull task that indicates the post to apply. This must encode the # new_topic data for this post so the apply_posts() function doesn't # need the models.Post entity in order to make progress. enqueue_post_task(shard, [post_id], new_topic=new_topic) # Notify all users of the post. futures = [] futures.append(ndb.transaction_async(txn)) futures.append(notify_posts(shard, [post])) # Set the dirty bit for this shard. This causes apply_posts to run a # second time if the Post transaction above completed while apply_posts # was already in flight. dirty_bit(shard, set=True) # Enqueue an apply task to sequence and notify the new post. futures.append(enqueue_apply_task(shard, post_id=post_id)) # Wait on futures in case they raise errors. ndb.Future.wait_all(futures) return post_key
def start_topic(root_shard_id, user_id, post_id, nickname, title, description): """Starts a new topic under a root shard.""" shard = models.Shard( id=models.human_uuid(), title=title, description=description, creation_nickname=nickname, root_shard=root_shard_id) shard.put() post_key = posts.insert_post( root_shard_id, post_id=post_id, archive_type=models.Post.TOPIC_START, nickname=nickname, user_id=user_id, title=title, body=description, new_topic=shard.shard_id) return shard.shard_id, post_key
def handle(self): shard = self.get_required('shard', str) email_address = self.get_required( 'email_address', unicode, '', html_escape=True) nickname = self.get_required('nickname', unicode, '', html_escape=True) accepted_terms = self.get_required('accepted_terms', str, '') == 'true' sounds_enabled = self.get_required('sounds_enabled', str, '') == 'true' retrying = self.get_required('retrying', str, '') == 'true' # Make sure this shard can be logged into. shard_record = models.Shard.get_by_id(shard) if shard_record and shard_record.root_shard: raise base.TopicShardError('Cannot login to topic shard') if 'shards' not in self.session: # First login on any shard with no cookie present. self.session['shards'] = {} user_id = self.session['shards'].get(shard) if not user_id: # First login to this shard. user_id = models.human_uuid() self.session['shards'][shard] = user_id user_connected, browser_token = change_presence( shard, user_id, nickname, accepted_terms, sounds_enabled, retrying, email_address) self.json_response['userConnected'] = user_connected self.json_response['browserToken'] = browser_token # Always assign the cookie on the top domain, so the user doesn't have # to accept the terms of service repeatedly. if not config.is_dev_appserver: host_parts = self.request.host.split('.') suffix = '.'.join(host_parts[-2:]) self.session.domain = '.' + suffix self.session.path = '/' self.session.save()
def send_digest_email(email_address, sequence_number): """Sends a digest email to the user with only what's changed.""" email_record = models.EmailRecord.get_or_insert( email_address, secret=models.human_uuid()) if email_record.global_opt_out: logging.debug('Not sending email to globally opted out address=%r', email_address) return if email_record.sequence_number > sequence_number: logging.warning( 'Saw email digest task for address=%r, sequence_number=%r but ' 'email record already at sequence_number=%r; dropping task', email_address, sequence_number, email_record.sequence_number) return # Find all shards the user participates in with this email address. query = models.LoginRecord.query() query = query.filter(models.LoginRecord.email_address == email_address) login_record_list = query.fetch(1000) shard_set = set(u.shard_id for u in login_record_list) # Generate the template rendering params for each shard and all of its # topics based on the email's read state for each topic. Do this in # parallel since none of them are interdependent. futures_dict = {} for root_shard_id in shard_set: futures_dict[root_shard_id] = get_topic_info( root_shard_id, email_address) ndb.Future.wait_all(futures_dict.values()) shard_list = [] for root_shard_id, future in futures_dict.iteritems(): topic_info_dict = future.get_result() if not topic_info_dict.get('topic_list'): logging.debug('No updates for shard=%r, email=%r', root_shard_id, email_address) continue shard_list.append(topic_info_dict) # Mark the email as processed so we can send future digests. def txn(): email_record = models.EmailRecord.get_by_id(email_address) email_record.sequence_number = sequence_number + 1 email_record.previous_notified_time = \ email_record.last_notified_time email_record.last_notified_time = datetime.datetime.now() email_record.put() return email_record email_record = ndb.transaction(txn) if not shard_list: logging.debug('No topics to digest for %s', email_address) return total_topics = 0 total_updates = 0 for shard_dict in shard_list: total_topics += shard_dict.get('total_topics', 0) total_updates += shard_dict.get('total_updates', 0) subject = 'What\'s new:' if total_topics == 1: subject += ' 1 topic' elif total_topics > 1: subject += ' %d topics' % total_topics if total_topics and total_updates: subject += ',' if total_updates == 1: subject += ' 1 update' elif total_updates > 1: subject += ' %d updates' % total_updates # Render the email content and send the email. We do this last so if # there are any bugs in the rendering code or sending step that it does # not result in us repeatedly sending emails to users. The transaction # above acts as a guard on this task. context = dict( cache_buster=config.version_id, email_record=email_record, email_resource_host_prefix=config.email_resource_host_prefix, email_title=subject, shard_list=shard_list, site_name=config.site_name ) text_data = template.render('templates/digest_email.txt', context) html_data = template.render('templates/digest_email_output.html', context) sender = config.notification_from_email logging.debug('Sending email digest to=%r, sender=%r, subject=%r', email_address, sender, subject) message = mail.EmailMessage( sender=sender, to=email_address, subject=subject, body=text_data, html=html_data) message.send()
def _require_xsrf_token(self): # TODO(bslatkin): Rotate the token periodically. if 'xsrf_token' not in self.session: self.session['xsrf_token'] = models.human_uuid() self.session.save()