Ejemplo n.º 1
0
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
Ejemplo n.º 2
0
 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
Ejemplo n.º 3
0
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
Ejemplo n.º 4
0
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
Ejemplo n.º 5
0
    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()
Ejemplo n.º 6
0
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()
Ejemplo n.º 7
0
 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()