From f55ef8652410b1fda86d73927528d375e6c6ad2a Mon Sep 17 00:00:00 2001
From: Ryan Barrett
Date: Fri, 30 Dec 2022 10:10:49 -0800
Subject: [PATCH] finish implementing getAuthorFeed
---
common.py | 3 -
tests/test_xrpc_actor.py | 27 +++---
tests/test_xrpc_feed.py | 203 +++++++++++++++++++++++++++------------
tests/test_xrpc_graph.py | 84 ++++++++--------
tests/testutil.py | 10 +-
xrpc_actor.py | 3 +-
xrpc_feed.py | 49 ++++++----
7 files changed, 236 insertions(+), 143 deletions(-)
diff --git a/common.py b/common.py
index 75e0257..662686f 100644
--- a/common.py
+++ b/common.py
@@ -18,6 +18,3 @@ DOMAIN_BLOCKLIST = frozenset((
't.co',
'twitter.com',
) + DOMAINS)
-
-# alias allows unit tests to mock the function
-utcnow = datetime.datetime.utcnow
diff --git a/tests/test_xrpc_actor.py b/tests/test_xrpc_actor.py
index 8f3344d..8d5383f 100644
--- a/tests/test_xrpc_actor.py
+++ b/tests/test_xrpc_actor.py
@@ -21,9 +21,9 @@ class XrpcActorTest(testutil.TestCase):
+ mock_get.return_value = requests_response(f"""\
+
+
+ Alice
+
+{POST_HTML}
+{REPLY_HTML}
+{REPOST_HTML}
-""", url='https://foo.com/')
+""", url='https://alice.com/')
- got = self.client.get('/xrpc/app.bsky.actor.getProfile',
- query_string={'actor': 'foo.com'},
- ).json
+ resp = self.client.get('/xrpc/app.bsky.feed.getAuthorFeed',
+ query_string={'author': 'alice.com'})
+ self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
self.assertEqual({
'feed': [{
- 'post': {
- },
+ '$type': 'app.bsky.feed.feedViewPost',
+ 'post': POST,
}, {
- 'post': {
- },
- 'reply': {
- },
+ '$type': 'app.bsky.feed.feedViewPost',
+ 'post': REPLY,
}, {
- 'post': {
- },
- 'reason': {
- 'by': '',
- 'indexedAt': testutil.NOW.isoformat(),
- }
+ '$type': 'app.bsky.feed.feedViewPost',
+ 'post': REPOST,
+ 'reason': REPOST_REASON,
}],
- }, got)
-
+ }, resp.json)
def test_getAuthorFeed_not_domain(self, _):
resp = self.client.get('/xrpc/app.bsky.feed.getAuthorFeed',
- query_string={'actor': 'not a domain'})
+ query_string={'author': 'not a domain'})
self.assertEqual(400, resp.status_code)
- def test_getPostThread(self, mock_get):
- mock_get.return_value = requests_response("""
-
-
-""", url='https://foo.com/')
+# def test_getPostThread(self, mock_get):
+# mock_get.return_value = requests_response("""
+#
+#
+# """, url='https://foo.com/')
- got = self.client.get('/xrpc/app.bsky.actor.getProfile',
- query_string={'actor': 'foo.com'},
- ).json
- self.assertEqual({
- }, got)
+# got = self.client.get('/xrpc/app.bsky.actor.getProfile',
+# query_string={'actor': 'foo.com'},
+# ).json
+# self.assertEqual({
+# }, got)
- def test_getRepostedBy(self, mock_get):
- mock_get.return_value = requests_response("""
-
-
-""", url='https://foo.com/')
+# def test_getRepostedBy(self, mock_get):
+# mock_get.return_value = requests_response("""
+#
+#
+# """, url='https://foo.com/')
- got = self.client.get('/xrpc/app.bsky.actor.getProfile',
- query_string={'actor': 'foo.com'},
- ).json
- self.assertEqual({
- }, got)
+# got = self.client.get('/xrpc/app.bsky.actor.getProfile',
+# query_string={'actor': 'foo.com'},
+# ).json
+# self.assertEqual({
+# }, got)
- def test_getTimeline(self, mock_get):
- mock_get.return_value = requests_response("""
-
-
-""", url='https://foo.com/')
+# def test_getTimeline(self, mock_get):
+# mock_get.return_value = requests_response("""
+#
+#
+# """, url='https://foo.com/')
- got = self.client.get('/xrpc/app.bsky.actor.getProfile',
- query_string={'actor': 'foo.com'},
- ).json
- self.assertEqual({
- }, got)
+# got = self.client.get('/xrpc/app.bsky.actor.getProfile',
+# query_string={'actor': 'foo.com'},
+# ).json
+# self.assertEqual({
+# }, got)
- def test_getVotes(self, mock_get):
- mock_get.return_value = requests_response("""
-
-
-""", url='https://foo.com/')
+# def test_getVotes(self, mock_get):
+# mock_get.return_value = requests_response("""
+#
+#
+# """, url='https://foo.com/')
- got = self.client.get('/xrpc/app.bsky.actor.getVotes',
- query_string={'actor': 'foo.com'},
- ).json
- self.assertEqual({
- }, got)
+# got = self.client.get('/xrpc/app.bsky.actor.getVotes',
+# query_string={'actor': 'foo.com'},
+# ).json
+# self.assertEqual({
+# }, got)
diff --git a/tests/test_xrpc_graph.py b/tests/test_xrpc_graph.py
index 6ce54af..1f01ebd 100644
--- a/tests/test_xrpc_graph.py
+++ b/tests/test_xrpc_graph.py
@@ -8,53 +8,53 @@ import requests
from . import testutil
-@patch('requests.get')
-class XrpcGraphTest(testutil.TestCase):
+# @patch('requests.get')
+# class XrpcGraphTest(testutil.TestCase):
- def test_getAuthorFeed(self, mock_get):
- mock_get.return_value = requests_response("""
-
-
-""", url='https://foo.com/')
+# def test_getAuthorFeed(self, mock_get):
+# mock_get.return_value = requests_response("""
+#
+#
+# """, url='https://foo.com/')
- got = self.client.get('/xrpc/app.bsky.actor.getProfile',
- query_string={'actor': 'foo.com'},
- ).json
- self.assertEqual({
- }, got)
+# got = self.client.get('/xrpc/app.bsky.actor.getProfile',
+# query_string={'actor': 'foo.com'},
+# ).json
+# self.assertEqual({
+# }, got)
- def test_getPostThread(self, mock_get):
- mock_get.return_value = requests_response("""
-
-
-""", url='https://foo.com/')
+# def test_getPostThread(self, mock_get):
+# mock_get.return_value = requests_response("""
+#
+#
+# """, url='https://foo.com/')
- got = self.client.get('/xrpc/app.bsky.actor.getProfile',
- query_string={'actor': 'foo.com'},
- ).json
- self.assertEqual({
- }, got)
+# got = self.client.get('/xrpc/app.bsky.actor.getProfile',
+# query_string={'actor': 'foo.com'},
+# ).json
+# self.assertEqual({
+# }, got)
- def test_getRepostedBy(self, mock_get):
- mock_get.return_value = requests_response("""
-
-
-""", url='https://foo.com/')
+# def test_getRepostedBy(self, mock_get):
+# mock_get.return_value = requests_response("""
+#
+#
+# """, url='https://foo.com/')
- got = self.client.get('/xrpc/app.bsky.actor.getProfile',
- query_string={'actor': 'foo.com'},
- ).json
- self.assertEqual({
- }, got)
+# got = self.client.get('/xrpc/app.bsky.actor.getProfile',
+# query_string={'actor': 'foo.com'},
+# ).json
+# self.assertEqual({
+# }, got)
- def test_getTimeline(self, mock_get):
- mock_get.return_value = requests_response("""
-
-
-""", url='https://foo.com/')
+# def test_getTimeline(self, mock_get):
+# mock_get.return_value = requests_response("""
+#
+#
+# """, url='https://foo.com/')
- got = self.client.get('/xrpc/app.bsky.actor.getProfile',
- query_string={'actor': 'foo.com'},
- ).json
- self.assertEqual({
- }, got)
+# got = self.client.get('/xrpc/app.bsky.actor.getProfile',
+# query_string={'actor': 'foo.com'},
+# ).json
+# self.assertEqual({
+# }, got)
diff --git a/tests/testutil.py b/tests/testutil.py
index 4bb3171..5c50c89 100644
--- a/tests/testutil.py
+++ b/tests/testutil.py
@@ -5,14 +5,13 @@ import unittest
import requests
from app import app, cache
-import common
from oauth_dropins.webutil import testutil, util
from oauth_dropins.webutil.appengine_config import ndb_client
from oauth_dropins.webutil.util import json_dumps, json_loads
-NOW = datetime.datetime(2022, 12, 24, 22, 29, 19)
-
+# can't use webutil.testutil.TestCase because it mock out requests.* with mox,
+# which collides with bridgy-at doing the same thing with unittest.mock.
class TestCase(unittest.TestCase, testutil.Asserts):
maxDiff = None
@@ -21,14 +20,15 @@ class TestCase(unittest.TestCase, testutil.Asserts):
app.testing = True
cache.clear()
self.client = app.test_client()
- self.xrpc = app.test_client()
- common.utcnow = lambda: NOW
# clear datastore
requests.post('http://%s/reset' % ndb_client.host)
+
self.ndb_context = ndb_client.context()
self.ndb_context.__enter__()
+ util.now = lambda **kwargs: testutil.NOW
+
def tearDown(self):
self.ndb_context.__exit__(None, None, None)
super().tearDown()
diff --git a/xrpc_actor.py b/xrpc_actor.py
index 4cb852a..21f53ce 100644
--- a/xrpc_actor.py
+++ b/xrpc_actor.py
@@ -31,10 +31,11 @@ def getProfile(input, actor=None):
logger.info(f'Representative h-card: {json.dumps(hcard, indent=2)}')
actor = microformats2.json_to_object(hcard)
+ actor.setdefault('url', url)
logger.info(f'AS1 actor: {json.dumps(actor, indent=2)}')
profile = {
- **bluesky.from_as1(actor, from_url=url),
+ **bluesky.from_as1(actor),
'myState': {
# ?
'follow': 'TODO',
diff --git a/xrpc_feed.py b/xrpc_feed.py
index ea50b1c..a5a5c95 100644
--- a/xrpc_feed.py
+++ b/xrpc_feed.py
@@ -1,5 +1,11 @@
"""app.bsky.feed.* XRPC methods."""
+import json
import logging
+import re
+
+from granary import microformats2, bluesky
+import mf2util
+from oauth_dropins.webutil import util
from app import xrpc_server
@@ -12,29 +18,36 @@ def getAuthorFeed(input, author=None, limit=None, before=None):
lexicons/app/bsky/feed/getAuthorFeed.json, feedViewPost.json
"""
if not re.match(util.DOMAIN_RE, author):
- raise ValueError(f'{actor} is not a domain')
+ raise ValueError(f'{author} is not a domain')
- url = f'https://{actor}/'
+ url = f'https://{author}/'
mf2 = util.fetch_mf2(url, gateway=True)
- hcard = mf2util.representative_hcard(mf2, mf2['url'])
- if not hcard:
- raise ValueError(f"Couldn't find a representative h-card (http://microformats.org/wiki/representative-hcard-parsing) on {mf2['url']}")
+ logger.info(f'Got mf2: {json.dumps(mf2, indent=2)}')
- logger.info(f'Representative h-card: {json.dumps(hcard, indent=2)}')
+ # hcard = mf2util.representative_hcard(mf2, mf2['url'])
+ feed_author = mf2util.find_author(mf2, source_url=url, fetch_mf2_func=util.fetch_mf2)
+ if feed_author:
+ logger.info(f'Authorship found: {feed_author}')
+ actor = {
+ 'url': feed_author.get('url') or url,
+ 'displayName': feed_author.get('name'),
+ 'image': {'url': feed_author.get('photo')},
+ }
+ else:
+ logger.info(f'No authorship result on {url} ; generated {feed_author}')
+ actor = {
+ 'url': url,
+ 'displayName': author,
+ }
- actor = microformats2.json_to_object(hcard)
- logger.info(f'AS1 actor: {json.dumps(actor, indent=2)}')
+ # actor = microformats2.json_to_object(author)
+ activities = microformats2.json_to_activities(mf2) #, actor)
+ # default actor to feed author
+ for a in activities:
+ a.setdefault('actor', actor)
+ logger.info(f'AS1 activities: {json.dumps(activities, indent=2)}')
- profile = {
- **bluesky.from_as1(actor, from_url=url),
- 'myState': {
- # ?
- 'follow': 'TODO',
- 'member': 'TODO',
- },
- }
- logger.info(f'Bluesky profile: {json.dumps(profile, indent=2)}')
- return profile
+ return {'feed': [bluesky.from_as1(a) for a in activities]}
@xrpc_server.method('app.bsky.feed.getPostThread')