diff --git a/pages.py b/pages.py index 46c82c8..86b4eea 100644 --- a/pages.py +++ b/pages.py @@ -5,7 +5,7 @@ import logging import os from flask import g, render_template, request -from google.cloud import ndb +from google.cloud.ndb import tasklets from google.cloud.ndb.query import AND, OR from google.cloud.ndb.stats import KindStat from granary import as1, as2, atom, microformats2, rss @@ -147,19 +147,24 @@ def feed(protocol, id): .fetch(PAGE_SIZE) activities = [obj.as1 for obj in objects if not obj.deleted] - # fill in authors, actors, objects stored in their own Objects + # hydrate authors, actors, objects from stored Objects fields = 'author', 'actor', 'object' - hydrate_ids = [id for id in itertools.chain( - *[[a[f] for f in fields if isinstance(a.get(f), str)] - for a in activities])] - if hydrate_ids: - keys = [ndb.Key(Object, id) for id in hydrate_ids] - hydrated = {o.key.id(): o.as1 for o in ndb.get_multi(keys) if o} - for a in activities: - for field in fields: - id = a.get(field) - if isinstance(id, str) and hydrated.get(id): - a[field] = hydrated[id] + gets = [] + for a in activities: + for field in fields: + val = as1.get_object(a, field) + if val and val.keys() <= set(['id']): + def hydrate(a, f): + def maybe_set(future): + if future.result() and future.result().as1: + a[f] = future.result().as1 + return maybe_set + + future = Object.get_by_id_async(val['id']) + future.add_done_callback(hydrate(a, field)) + gets.append(future) + + tasklets.wait_all(gets) actor = { 'displayName': id, diff --git a/tests/test_pages.py b/tests/test_pages.py index 5e7fe6b..ea7f581 100644 --- a/tests/test_pages.py +++ b/tests/test_pages.py @@ -1,15 +1,10 @@ """Unit tests for pages.py.""" from google.cloud import ndb from granary import atom, microformats2, rss -from granary.tests.test_as1 import ( - ACTOR, - COMMENT, - NOTE, -) from oauth_dropins.webutil import util # import first so that Fake is defined before URL routes are registered -from .testutil import Fake, TestCase +from .testutil import Fake, TestCase, ACTOR, COMMENT, MENTION, NOTE from activitypub import ActivityPub from models import Object, Follower @@ -24,11 +19,12 @@ ACTOR_WITH_PREFERRED_USERNAME = { def contents(activities): - return [(a.get('object') or a)['content'] for a in activities] + return [(a.get('object') or a)['content'].splitlines()[0] + for a in activities] class PagesTest(TestCase): - EXPECTED = contents([COMMENT, NOTE]) + EXPECTED = contents([COMMENT, NOTE, NOTE]) def setUp(self): super().setUp() @@ -254,19 +250,6 @@ class PagesTest(TestCase): def test_feed_html(self): self.add_objects() - # note with author in separate Object - note_2 = { - 'objectType': 'note', - 'content': 'foo', - 'author': 'fake:alice', - } - alice = { - 'displayName': 'Ms Alice Macbeth', - } - user = ndb.Key(Web, 'user.com') - self.store_object(id='fake:note_2', feed=[user], our_as1=note_2) - self.store_object(id='fake:alice', our_as1=alice) - # repost with object (original post) in separate Object repost = { 'objectType': 'activity', @@ -277,15 +260,17 @@ class PagesTest(TestCase): 'objectType': 'note', 'content': 'biff', } - self.store_object(id='fake:repost', feed=[user], our_as1=repost) + self.store_object(id='fake:repost', feed=[self.user.key], our_as1=repost) self.store_object(id='fake:orig', our_as1=orig) got = self.client.get('/web/user.com/feed') self.assert_equals(200, got.status_code) - self.assert_equals(self.EXPECTED + ['foo', 'biff'], + self.assert_equals(['biff'] + self.EXPECTED, contents(microformats2.html_to_activities(got.text))) - self.assertIn('Ms Alice Macbeth', got.text) - self.assertIn('biff', got.text) + + # NOTE's and MENTION's authors; check for two instances + bob = 'Bob' + assert got.text.index(bob) != got.text.rindex(bob) def test_feed_atom_empty(self): got = self.client.get('/web/user.com/feed?format=atom') @@ -298,6 +283,16 @@ class PagesTest(TestCase): self.assert_equals(200, got.status_code) self.assert_equals(self.EXPECTED, contents(atom.atom_to_activities(got.text))) + # NOTE's and MENTION's authors; check for two instances + bob = """ + https://plus.google.com/bob + + Bob +""" + assert got.text.index(bob) != got.text.rindex(bob) + # COMMENT's author + self.assertIn('Dr. Eve', got.text) + def test_feed_rss_empty(self): got = self.client.get('/web/user.com/feed?format=rss') self.assert_equals(200, got.status_code) @@ -309,6 +304,12 @@ class PagesTest(TestCase): self.assert_equals(200, got.status_code) self.assert_equals(self.EXPECTED, contents(rss.to_activities(got.text))) + # NOTE's and MENTION's authors; check for two instances + bob = '- (Bob)' + assert got.text.index(bob) != got.text.rindex(bob) + # COMMENT's author + self.assertIn('Dr. Eve', got.text) + def test_nodeinfo(self): # just check that it doesn't crash self.client.get('/nodeinfo.json') diff --git a/tests/testutil.py b/tests/testutil.py index 1b2fab4..603e4f8 100644 --- a/tests/testutil.py +++ b/tests/testutil.py @@ -16,6 +16,7 @@ from flask import g from google.cloud import ndb from granary import as2 from granary.tests.test_as1 import ( + ACTOR, COMMENT, MENTION, NOTE, @@ -33,6 +34,25 @@ import protocol logger = logging.getLogger(__name__) +NOTE = { + **NOTE, + # bare string author id + 'author': ACTOR['id'], +} +MENTION = { + **MENTION, + # author object with just id + 'author': {'id': ACTOR['id']}, +} +COMMENT = { + **COMMENT, + # full author object + 'author': { + **ACTOR, + 'displayName': 'Dr. Eve', + }, +} + class Fake(User, protocol.Protocol): ABBREV = 'fa' @@ -237,25 +257,36 @@ class TestCase(unittest.TestCase, testutil.Asserts): users=[user], notify=[user], feed=[user], - as2=as2.from_as1(NOTE)) - # different domain - nope = ndb.Key(Web, 'nope.org') + our_as1=NOTE) + # post with mention self.store_object(id='b', - notify=[nope], - feed=[nope], - as2=as2.from_as1(MENTION)) + notify=[user], + feed=[user], + our_as1=MENTION) # reply self.store_object(id='d', notify=[user], feed=[user], - as2=as2.from_as1(COMMENT)) + our_as1=COMMENT) # not feed/notif - self.store_object(id='e', users=[user], as2=as2.from_as1(NOTE)) + self.store_object(id='e', + users=[user], + our_as1=NOTE) # deleted self.store_object(id='f', notify=[user], feed=[user], - as2=as2.from_as1(NOTE), deleted=True) + our_as1=NOTE, + deleted=True) + # different domain + nope = ndb.Key(Web, 'nope.org') + self.store_object(id='g', + notify=[nope], + feed=[nope], + our_as1=MENTION) + + # actor whose id is in NOTE.author + self.store_object(id=ACTOR['id'], our_as1=ACTOR) @staticmethod def store_object(**kwargs):