示例#1
0
 def root(self):
     session = self.request.session if self.request.session else None
     user = self.request.user if self.request.user else None
     profiles = None
     emails = None
     if user:
         profile_keys = [ndb.Key("UserProfile", p) for p in user.auth_ids]
         profiles = ndb.get_multi(profile_keys)
         emails = models.UserEmail.get_by_user(user.key.id())
     self.render_template("home.html", {"user": user, "session": session, "profiles": profiles, "emails": emails})
示例#2
0
文件: posts.py 项目: bslatkin/8-bits
    def handle(self):
        start = self.get_required('start', int, 0)
        end = self.get_required('end', int, 0)
        count = self.get_required('count', int, 100)

        query = models.PostReference.query()

        if not start and not end:
            # Get newest posts by doing a prefix scan.
            start_key = ndb.Key(
                models.Shard._get_kind(), self.shard,
                models.PostReference._get_kind(), 1)
            end_key = ndb.Key(
                models.Shard._get_kind(), self.shard,
                models.PostReference._get_kind(), 2**62)
        elif not start and end:
            # Get a specific set of posts
            start_key = ndb.Key(
                models.Shard._get_kind(), self.shard,
                models.PostReference._get_kind(), max(1, end - count))
            end_key = ndb.Key(
                models.Shard._get_kind(), self.shard,
                models.PostReference._get_kind(), end)
        else:
            # Should not happen
            assert False

        query = query.filter(models.PostReference.key >= start_key)
        query = query.filter(models.PostReference.key <= end_key)
        query = query.order(-models.PostReference.key)

        ref_list = query.fetch(count)
        post_kind = models.Post._get_kind()

        post_key_list = [ndb.Key(post_kind, ref.post_id) for ref in ref_list]
        post_list = ndb.get_multi(post_key_list)
        adjusted_post_list = []
        for post, ref in zip(post_list, ref_list):
            # PostReference entities may point to non-existent Post entities
            # once the cleanup job has run. Filter them out here. The client
            # side won't try to scan for posts previous to the last one that's
            # actually found, so this filtering is okay.
            if not post:
                continue

            # TODO(bslatkin): Drop post entities that are older than
            # config.ephemeral_lifetime_seconds, in case the background job
            # takes longer than expected to delete things.

            post.sequence = ref.sequence
            adjusted_post_list.append(post)

        self.json_response['posts'] = marshal_posts(
            self.shard, adjusted_post_list)
 def post(self):
     email = self.request.get('email')
     if not email:
         # validate email address
         # TODO: use WTForm
         self.add_message('Please provide an email address', 'error')
         return self.redirect_to('password-reset')
     
     # validate existence of email address
     auth_id = models.User.generate_auth_id('email', email)
     user = models.User._find_user(auth_id, [email])
     
     if not user:
         self.add_message('No one with that email address was found', 'error')
         return self.redirect_to('password-reset')
         
     profiles = ndb.get_multi(ndb.Key(models.UserProfile, key) for key in user.auth_ids)
     
     if not profiles:
         self.add_message('No one with that email address was found', 'error')
         return self.redirect_to('password-reset')
     
     pwd = None
     for profile in profiles:
         if profile.password:
             pwd = profile.password
     
     if pwd:
         # generate and save reset_token
         timestamp = str(time.time())
         token_value = security.generate_password_hash(pwd+timestamp, length=12)
 
         callback_uri = "%(uri)s?ts=%(ts)s&token=%(token)s&email=%(email)s" %dict(uri=webapp2.uri_for('new-password', _full=True),
                                                                                  ts=timestamp, email=email, token=token_value)
         
         # send validation email
         from google.appengine.api import mail
         
         mail.send_mail(sender="Example.com Support <*****@*****.**>",
                       to=email,
                       subject="Password reset",
                       body="""
         Dear %(email)s:
         
         Click on the following link to reset your password %(callback)s
         
         Please let us know if you have any questions.
         
         The example.com Team
         """%dict(email=email, callback=callback_uri))
         self.add_message('We sent you an email with instructions to reset your password')
     else:
         self.add_message('You previously associated %(providers)s' %dict(providers=', '.join([profile.key.id() for profile in profiles])))
     return self.redirect_to('password-reset')
示例#4
0
文件: topics.py 项目: bslatkin/8-bits
    def txn():
        read_state_list = ndb.get_multi(read_state_keys)
        to_put = []
        for key, read_state in zip(read_state_keys, read_state_list):
            next_read_sequence = topic_dict[key.id()]
            if read_state is None:
                read_state = models.ReadState(key=key)
                read_state.last_read_sequence = next_read_sequence
            else:
                read_state.last_read_sequence = max(
                    read_state.last_read_sequence, next_read_sequence)

            to_put.append(read_state)

        ndb.put_multi(to_put)
    def get(self):
        profiles = None
        emails = None
        user = self.user
        
        if user:
            profiles_done = [p[:p.index(':')] for p in user.auth_ids]
            profile_keys = [ndb.Key('UserProfile', p) for p in user.auth_ids]
            profiles = ndb.get_multi(profile_keys)

            emails = models.UserEmail.get_by_user(user.key.id())
        self.render_template('profile.html', {
                'profiles': profiles,
                'profiles_done': profiles_done,
                'emails': emails,
                'form': self.form,
            })
示例#6
0
def enqueue_email_tasks(emails_set):
    """Enqueues a set of email notification tasks for the given users.

    Notifies these users across multiple shards.

    Args:
        emails_set: Set of user email addresses to notify.
    """
    if not emails_set:
        logging.debug('No email addresses to notify')
        return

    logging.debug('Found %d email addresses to notify', len(emails_set))
    email_record_keys = [
        ndb.Key(models.EmailRecord._get_kind(), email_address)
        for email_address in emails_set]
    email_record_list = ndb.get_multi(email_record_keys)

    task_list = []
    for email_address, email_record in zip(emails_set, email_record_list):
        sequence_number = 1
        countdown = config.default_notify_period_seconds
        if email_record:
            sequence_number = email_record.sequence_number
            countdown = email_record.min_notify_period_seconds

        task = taskqueue.Task(
            url='/work/email_digest',
            params=dict(sequence_number=sequence_number,
                        email_address=email_address),
            name='email-notify-%s-%s' % (
                models.human_hash(email_address), sequence_number),
            countdown=countdown)
        task_list.append(task)

    logging.debug('Enqueuing email notification tasks for %d users',
                  len(task_list))
    try:
        taskqueue.Queue(config.email_digest_queue).add(task_list)
    except (taskqueue.TombstonedTaskError, taskqueue.TaskAlreadyExistsError):
        logging.debug('Enqueued email tasks for emails=%r; '
                      'at least one task already present', emails_set)
示例#7
0
文件: posts.py 项目: bslatkin/8-bits
def apply_posts(shard=None,
                insertion_post_id=None,
                lease_seconds=10,
                max_tasks=20):
    """Applies a set of pending posts to a shard.

    If shard is None then this function will apply mods for whatever is the
    first shard it can find in the pull task queue.

    insertion_post_id is the post_id that first caused this apply task to be
    enqueued. This task will retry until it applies the insertion_post_id
    itself or it can confirm that the insertion_post_id has already been
    applied. insertion_post_id may be empty if the apply task is not associated
    with a particular post (such as cronjobs/cleanup tasks).
    """
    # Do not use caching for NDB in this task queue worker.
    ctx = ndb.get_context()
    ctx.set_cache_policy(lambda x: False)
    ctx.set_memcache_policy(lambda x: False)

    # Fetch the new Posts to put in sequence.
    queue = taskqueue.Queue(config.pending_queue)

    # When no shard is specified, process the first tag we find.
    task_list = []
    if not shard:
        task_list.extend(queue.lease_tasks(lease_seconds, 1))
        if not task_list:
            logging.debug('apply_posts with no specific shard found no tasks')
            return
        params = task_list[0].extract_params()
        shard = params['shard']
        logging.debug('apply_posts with no specific shard found shard=%r',
                      shard)

    # Clear the dirty bit on this shard to start the time horizon.
    dirty_bit(shard, clear=True)

    # Find tasks pending for the current shard.
    task_list.extend(
        queue.lease_tasks_by_tag(lease_seconds, max_tasks, tag=str(shard)))

    receipt_key_list = []
    new_topic = None
    for task in task_list:
        params = task.extract_params()

        # Extract the new topic shard associated with this task, if any. The
        # last one wins. If all of the found posts have already been applied,
        # then topic assignment will be ignored.
        new_topic = params.get('new_topic') or new_topic

        post_id_list = params.get('post_ids')
        if post_id_list is None:
            # This may happen on replica shards if it turns out there are no
            # unapplied post IDs but an apply task still ran.
            post_id_list = []
        elif not isinstance(post_id_list, list):
            post_id_list = [post_id_list]

        for post_id in post_id_list:
            receipt_key = ndb.Key(
                models.Post._get_kind(), post_id,
                models.Receipt._get_kind(), shard)
            receipt_key_list.append(receipt_key)

    receipt_list = ndb.get_multi(receipt_key_list)

    # Some tasks may be in the pull queue that were already put in sequence.
    # So ignore these and only apply the new ones.
    unapplied_receipts = [
        models.Receipt(key=k)
        for k, r in zip(receipt_key_list, receipt_list)
        if r is None]
    unapplied_post_ids = [r.post_id for r in unapplied_receipts]

    # Double check if we think there should be work to apply but we didn't find
    # any. This will force the apply task to retry immediately if the post task
    # was not found. This can happen when the pull queue's consistency is
    # behind.
    if not unapplied_receipts and insertion_post_id:
        receipt_key = ndb.Key(
            models.Post._get_kind(), insertion_post_id,
            models.Receipt._get_kind(), shard)
        receipt = receipt_key.get()
        if receipt:
            logging.warning(
                'No post application to do for shard=%r, but post_id=%r '
                'already applied; doing nothing in this task',
                shard, insertion_post_id)
            new_topic = None
            # Do not 'return' here. We need to increment the shard sequence or
            # else tasks will not run for this shard in the future because of
            # de-duping.
        else:
            raise base.Error('No post application to do for shard=%r, but'
                             'post_id=%r has not been applied; will retry' %
                             (shard, insertion_post_id))

    now = datetime.datetime.now()

    def txn():
        shard_record = models.Shard.get_by_id(shard)
        # TODO(bslatkin): Just drop this task entirely if the shard cannot
        # be found. Could happen for old shards that were cleaned up.
        assert shard_record

        # One of the tasks in this batch has a topic assignment. Apply it here.
        if new_topic:
            logging.debug('Changing topic from %r to %r',
                          shard_record.current_topic, new_topic)
            shard_record.current_topic = new_topic
            shard_record.topic_change_time = now

        new_sequence_numbers = list(xrange(
            shard_record.sequence_number,
            shard_record.sequence_number + len(unapplied_receipts)))
        shard_record.sequence_number += max(1, len(unapplied_receipts))

        # Write post references that point at the newly sequenced posts.
        to_put = [shard_record]
        for receipt, sequence in zip(unapplied_receipts, new_sequence_numbers):
            to_put.append(models.PostReference(
                id=sequence,
                parent=shard_record.key,
                post_id=receipt.post_id))
            # Update the receipt entity here; it will be written outside this
            # transaction, since these receipts may span multiple entity
            # groups.
            receipt.sequence = sequence

        # Enqueue replica posts transactionally, to make sure everything
        # definitely will get copied over to the replica shard.
        if shard_record.current_topic:
            enqueue_post_task(shard_record.current_topic, unapplied_post_ids)

        ndb.put_multi(to_put)

        return shard_record, new_sequence_numbers

    # Have this only attempt a transaction a single time. If the transaction
    # fails the task queue will retry this task within 4 seconds. Because
    # apply tasks are always named by the current Shard.sequence_number we
    # can be reasonably sure that no other apply task for this shard will be
    # running concurrently when this fails.
    shard_record, new_sequence_numbers = ndb.transaction(txn, retries=1)
    replica_shard = shard_record.current_topic

    logging.debug('Applied %d posts for shard=%r, sequence_numbers=%r',
                  len(unapplied_receipts), shard, new_sequence_numbers)

    futures = []

    # Save receipts for all the posts.
    futures.extend(ndb.put_multi_async(unapplied_receipts))

    # Notify all logged in users of the new posts.
    futures.append(notify_posts(
        shard, unapplied_post_ids, sequence_numbers=new_sequence_numbers))

    # Replicate posts to a topic shard.
    if replica_shard:
        logging.debug('Replicating source shard=%r to replica shard=%r',
                      shard, replica_shard)
        futures.append(enqueue_apply_task(replica_shard))

    # Success! Delete the tasks from this queue.
    queue.delete_tasks(task_list)

    # Always run one more apply task to clean up any posts that came in
    # while this transaction was processing.
    if dirty_bit(shard, check=True):
        futures.append(enqueue_apply_task(shard))

    # Wait on all pending futures in case they raise errors.
    ndb.Future.wait_all(futures)

    # For root shards, add shard cleanup task to check for user presence and
    # cause notification of user logouts if the channel API did not detect the
    # user closing the connection.
    if not shard_record.root_shard:
        presence.enqueue_cleanup_task(shard)
示例#8
0
    def handle_request(self, shard_id):
        shard = models.Shard.get_by_id(shard_id)
        if not shard:
            # If the shard doesn't exist, then just create it. Makes it
            # ridiculously easy for people to create a new chat with the name
            # of their choice.
            def txn():
                shard = models.Shard.get_by_id(
                    shard_id, use_cache=False, use_memcache=False)
                if not shard:
                    shard = models.Shard(id=shard_id)
                    shard.put()
                return shard

            shard = ndb.transaction(txn)

        # Do not allow users to directly access topic shards.
        if shard.root_shard:
            # TODO(bslatkin): Make this pretty.
            self.response.set_status(404)
            return

        nickname = 'Anonymous'
        email_address = ''
        first_login = True
        must_accept_terms = True
        sounds_enabled = True

        if 'shards' in self.session:
            # TODO(bslatkin): Reuse presence code here.
            user_id = self.session['shards'].get(shard_id)
            if user_id:
                login_record = models.LoginRecord.get_by_id(user_id)
                if login_record and login_record.shard_id == shard_id:
                    email_address = login_record.email_address
                    nickname = login_record.nickname
                    first_login = False
                    must_accept_terms = bool(
                        login_record.accepted_terms_version !=
                        config.terms_version)
                    sounds_enabled = login_record.sounds_enabled
            else:
                # Pre-populate the user's settings info from the newest other
                # shard the user has.
                all_user_ids = self.session['shards'].values()
                all_login_records = ndb.get_multi([
                    ndb.Key(models.LoginRecord._get_kind(), user_id)
                    for user_id in all_user_ids])
                all_login_records = [r for r in all_login_records if r]
                all_login_records.sort(key=lambda x: x.last_update_time)
                if all_login_records:
                    login_record = all_login_records[-1]
                    email_address = login_record.email_address
                    nickname = login_record.nickname
                    sounds_enabled = login_record.sounds_enabled

        context = {
            'email_address': email_address or '',
            'first_login': first_login,
            'must_accept_terms': must_accept_terms,
            'nickname': xml.sax.saxutils.unescape(nickname),
            'page_name': 'chatroom',
            'shard_id': shard_id,
            'short_url': self.request.path_url,
            'sounds_enabled': sounds_enabled,
            'xsrf_token': self.session['xsrf_token'],
        }
        context['params'] = json.dumps(context)

        self.render('chatroom.html', context)