From 2b9f2d8f752545a79090c0e51d19bbbd4d75c635 Mon Sep 17 00:00:00 2001 From: Joel Auterson Date: Mon, 24 Jul 2023 20:17:40 +0100 Subject: [PATCH 01/12] Implement Bluesky changes needed for backfeed --- granary/bluesky.py | 174 +++++++++++++++++++++++++++++++--- granary/tests/test_bluesky.py | 6 +- 2 files changed, 166 insertions(+), 14 deletions(-) diff --git a/granary/bluesky.py b/granary/bluesky.py index ace5ce33..da72d4e1 100644 --- a/granary/bluesky.py +++ b/granary/bluesky.py @@ -4,7 +4,6 @@ * https://atproto.com/lexicons/app-bsky-actor * https://github.com/bluesky-social/atproto/tree/main/lexicons/app/bsky """ -import copy import json import logging import re @@ -582,7 +581,6 @@ def to_as1(obj, type=None, repo_did=None, repo_handle=None, pds=DEFAULT_PDS): kwargs = {'repo_did': repo_did, 'repo_handle': repo_handle, 'pds': pds} - # TODO: once we're on Python 3.10, switch this to a match statement! if type in ('app.bsky.actor.profile', 'app.bsky.actor.defs#profileView', 'app.bsky.actor.defs#profileViewBasic', @@ -771,6 +769,7 @@ def to_as1(obj, type=None, repo_did=None, repo_handle=None, pds=DEFAULT_PDS): reason = obj.get('reason') if reason and reason.get('$type') == 'app.bsky.feed.defs#reasonRepost': ret = { + 'id': obj.get('post', {}).get('viewer', {}).get('repost'), 'objectType': 'activity', 'verb': 'share', 'object': ret, @@ -992,15 +991,166 @@ def get_activities_response(self, user_id=None, group_id=None, app_id=None, resp = self.client.app.bsky.feed.getAuthorFeed({}, actor=handle, **params) posts = resp.get('feed', []) - # TODO: inReplyTo - ret = self.make_activities_base_response( - trim_nulls(to_as1(post, type='app.bsky.feed.defs#feedViewPost', - repo_did=self.did, repo_handle=handle)) - for post in posts - ) - ret['actor'] = { - 'id': self.did, - 'displayName': self.handle, - 'url': self.user_url(self.handle), + if cache is None: + # for convenience, throwaway object just for this method + cache = {} + + activities = [] + + for post in posts: + reason = post.get('reason') + is_repost = reason and reason.get('$type') == 'app.bsky.feed.defs#reasonRepost' + if is_repost and not include_shares: + continue + + activity = self.postprocess_activity(self._post_to_activity(post)) + activities.append(activity) + obj = activity['object'] + id = obj.get('id') + tags = obj.setdefault('tags', []) + + if is_repost: + # If it's a repost we're not interested in responses to it. + continue + bs_post = post.get('post') + if bs_post and id: + # Likes + like_count = bs_post.get('likeCount') + if fetch_likes and like_count and like_count != cache.get('ABL ' + id): + likers = self.client.app.bsky.feed.getLikes({}, uri=bs_post.get('uri')) + tags.extend(self._make_like(bs_post, l.get('actor')) for l in likers.get('likes')) + cache['ABL ' + id] = count + + # Reposts + repost_count = bs_post.get('repostCount') + if fetch_shares and repost_count and repost_count != cache.get('ABRP ' + id): + reposters = self.client.app.bsky.feed.getRepostedBy({}, uri=bs_post.get('uri')) + tags.extend(self._make_share(bs_post, r) for r in reposters.get('repostedBy')) + cache['ABRP ' + id] = count + + # Replies + reply_count = bs_post.get('replyCount') + if fetch_replies and reply_count and reply_count != cache.get('ABR ' + id): + replies = self._get_replies(bs_post.get('uri')) + replies = [to_as1(reply, 'app.bsky.feed.defs#threadViewPost') for reply in replies] + for r in replies: + r['id'] = self.tag_uri(r['id']) + obj['replies'] = { + 'items': replies, + } + cache['ABR ' + id] = count + + resp = self.make_activities_base_response(util.trim_nulls(activities)) + return resp + + def user_to_actor(self, user): + """Converts a dict user to an actor. + + Args: + user: Bluesky user app.bsky.actor.defs#profileViewDetailed + + Returns: + an ActivityStreams actor dict, ready to be JSON-encoded + """ + return to_as1(user) + + def get_comment(self, comment_id, **kwargs): + """Fetches and returns a comment. + + Args: + comment_id: string status id + **kwargs: unused + + Returns: dict, ActivityStreams object + + Raises: + :class:`ValueError`: if comment_id is invalid + """ + post_thread = self.client.app.bsky.feed.getPostThread({}, uri=comment_id) + obj = to_as1(post_thread.get('thread'), 'app.bsky.feed.defs#threadViewPost') + return obj + + def _post_to_activity(self, post): + """Converts a post to an activity. + + Args: + post: Bluesky post app.bluesky.feed.defs#feedViewPost + + Returns: AS1 activity + """ + obj = to_as1(post, type='app.bsky.feed.defs#feedViewPost') + if obj.get('objectType') == 'activity': + return obj + return { + 'id': obj.get('id'), + 'verb': 'post', + 'actor': obj.get('author'), + 'object': obj, + 'objectType': 'activity', + 'context': {'inReplyTo': obj.get('inReplyTo')}, } + + def _make_like(self, post, actor): + return self._make_like_or_share(post, actor, 'like') + + def _make_share(self, post, actor): + return self._make_like_or_share(post, actor, 'share') + + def _make_like_or_share(self, post, actor, verb): + """Generates and returns a ActivityStreams like object. + + Args: + post: dict, Bluesky app.bsky.feed.defs#feedViewPost + actor: dict, Bluesky app.bsky.actor.defs#profileView + verb: string, 'like' or 'share' + + Returns: dict, AS1 like activity + """ + assert verb in ('like', 'share') + label = 'liked' if verb == 'like' else 'reposted' + url = at_uri_to_web_url(post.get('uri'), post.get('author').get('handle')) + actor_id = actor.get('did') + author = to_as1(actor, 'app.bsky.actor.defs#profileView') + author['id'] = self.tag_uri(author['id']) + return { + 'id': self.tag_uri(f"{post.get('uri')}_{label}_by_{actor_id}"), + 'url': url, + 'objectType': 'activity', + 'verb': verb, + 'object': {'url': url}, + 'author': author, + } + + def _get_replies(self, uri): + """ + Gets the replies to a specific post and returns them + in ascending order of creation. Does not include the original post. + + Args: + uri: string, post uri + + Returns: list, Bluesky app.bsky.feed.defs#threadViewPost + """ + ret = [] + resp = self.client.app.bsky.feed.getPostThread({}, uri=uri) + thread = resp.get('thread') + if thread: + ret = self._recurse_replies(thread) + return sorted(ret, key = lambda thread: thread.get('post', {}).get('record', {}).get('createdAt')) + + # TODO this ought to take a depth limit. + def _recurse_replies(self, thread): + """ + Recurses through a Bluesky app.bsky.feed.defs#threadViewPost + and returns its replies as a list. + + Args: + thread: dict, Bluesky app.bsky.feed.defs#threadViewPost + + Returns: list, Bluesky app.bsky.feed.defs#threadViewPost + """ + ret = [] + for r in thread.get('replies', []): + ret += [r] + ret += self._recurse_replies(r) return ret diff --git a/granary/tests/test_bluesky.py b/granary/tests/test_bluesky.py index 36693d58..a7116f27 100644 --- a/granary/tests/test_bluesky.py +++ b/granary/tests/test_bluesky.py @@ -60,7 +60,9 @@ POST_AS = { 'objectType': 'activity', + 'id': 'at://did/app.bsky.feed.post/tid', 'verb': 'post', + 'actor': ACTOR_AS, 'object': { 'objectType': 'note', 'id': 'at://did/app.bsky.feed.post/tid', @@ -789,7 +791,7 @@ def test_get_activities_activity_id(self, mock_get): 'replies': [REPLY_BSKY], }) - self.assert_equals([POST_AUTHOR_PROFILE_AS['object']], + self.assert_equals([POST_AUTHOR_PROFILE_AS], self.bs.get_activities(activity_id='at://id')) mock_get.assert_called_once_with( 'https://bsky.social/xrpc/app.bsky.feed.getPostThread?uri=at%3A%2F%2Fid&depth=1', @@ -812,7 +814,7 @@ def test_get_activities_self_user_id(self, mock_get): 'feed': [POST_AUTHOR_BSKY], }) - self.assert_equals([POST_AUTHOR_PROFILE_AS['object']], + self.assert_equals([POST_AUTHOR_PROFILE_AS], self.bs.get_activities(group_id=SELF, user_id='alice.com')) mock_get.assert_called_once_with( 'https://bsky.social/xrpc/app.bsky.feed.getAuthorFeed?actor=alice.com', From a47a26e13ab9bdfb19a2d0067c75f4e1eb74ffee Mon Sep 17 00:00:00 2001 From: Joel Auterson Date: Sat, 7 Oct 2023 14:17:06 +0100 Subject: [PATCH 02/12] Fix tests after rebase --- granary/tests/test_bluesky.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/granary/tests/test_bluesky.py b/granary/tests/test_bluesky.py index a7116f27..3adc569a 100644 --- a/granary/tests/test_bluesky.py +++ b/granary/tests/test_bluesky.py @@ -106,6 +106,10 @@ 'username': 'alice.com', 'url': 'https://bsky.app/profile/alice.com', }) +POST_AUTHOR_PROFILE_AS['actor'].update({ + 'username': 'alice.com', + 'url': 'https://bsky.app/profile/alice.com', +}) POST_AUTHOR_BSKY = copy.deepcopy(POST_VIEW_BSKY) POST_AUTHOR_BSKY['author'] = { **ACTOR_PROFILE_VIEW_BSKY, @@ -770,7 +774,7 @@ def test_get_activities_friends(self, mock_get): expected_repost = copy.deepcopy(REPOST_AS) expected_repost['actor']['username'] = 'bob.com' - self.assert_equals([POST_AUTHOR_PROFILE_AS['object'], expected_repost], + self.assert_equals([POST_AUTHOR_PROFILE_AS, expected_repost], self.bs.get_activities(group_id=FRIENDS)) mock_get.assert_called_once_with( From d73051a25b3fab272a1924b77185534a1039a24c Mon Sep 17 00:00:00 2001 From: Joel Auterson Date: Fri, 20 Oct 2023 09:36:06 +0100 Subject: [PATCH 03/12] Remove redundant user_to_actor --- granary/bluesky.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/granary/bluesky.py b/granary/bluesky.py index da72d4e1..b6a19d70 100644 --- a/granary/bluesky.py +++ b/granary/bluesky.py @@ -1043,17 +1043,6 @@ def get_activities_response(self, user_id=None, group_id=None, app_id=None, resp = self.make_activities_base_response(util.trim_nulls(activities)) return resp - def user_to_actor(self, user): - """Converts a dict user to an actor. - - Args: - user: Bluesky user app.bsky.actor.defs#profileViewDetailed - - Returns: - an ActivityStreams actor dict, ready to be JSON-encoded - """ - return to_as1(user) - def get_comment(self, comment_id, **kwargs): """Fetches and returns a comment. From 465c6c2f6ef154e7a978cbfd6bcf3eccd874a258 Mon Sep 17 00:00:00 2001 From: Joel Auterson Date: Fri, 20 Oct 2023 09:37:10 +0100 Subject: [PATCH 04/12] Add back TODO that was inadvertently removed --- granary/bluesky.py | 1 + 1 file changed, 1 insertion(+) diff --git a/granary/bluesky.py b/granary/bluesky.py index b6a19d70..ebf76bc9 100644 --- a/granary/bluesky.py +++ b/granary/bluesky.py @@ -581,6 +581,7 @@ def to_as1(obj, type=None, repo_did=None, repo_handle=None, pds=DEFAULT_PDS): kwargs = {'repo_did': repo_did, 'repo_handle': repo_handle, 'pds': pds} + # TODO: once we're on Python 3.10, switch this to a match statement! if type in ('app.bsky.actor.profile', 'app.bsky.actor.defs#profileView', 'app.bsky.actor.defs#profileViewBasic', From 54df15a65b8cdc0ffb852dc58bcddca5fcca92ad Mon Sep 17 00:00:00 2001 From: Joel Auterson Date: Sat, 21 Oct 2023 13:25:03 +0100 Subject: [PATCH 05/12] Add depth=1 to get_comment, add test --- granary/bluesky.py | 2 +- granary/tests/test_bluesky.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/granary/bluesky.py b/granary/bluesky.py index ebf76bc9..15559aeb 100644 --- a/granary/bluesky.py +++ b/granary/bluesky.py @@ -1056,7 +1056,7 @@ def get_comment(self, comment_id, **kwargs): Raises: :class:`ValueError`: if comment_id is invalid """ - post_thread = self.client.app.bsky.feed.getPostThread({}, uri=comment_id) + post_thread = self.client.app.bsky.feed.getPostThread({}, uri=comment_id, depth=1) obj = to_as1(post_thread.get('thread'), 'app.bsky.feed.defs#threadViewPost') return obj diff --git a/granary/tests/test_bluesky.py b/granary/tests/test_bluesky.py index 3adc569a..d60f78cf 100644 --- a/granary/tests/test_bluesky.py +++ b/granary/tests/test_bluesky.py @@ -58,6 +58,10 @@ 'description': 'hi there', } +POST_OBJ_AS = { + +} + POST_AS = { 'objectType': 'activity', 'id': 'at://did/app.bsky.feed.post/tid', @@ -829,3 +833,24 @@ def test_get_activities_self_user_id(self, mock_get): 'User-Agent': util.user_agent, }, ) + + @patch('requests.get') + def test_get_comment(self, mock_get): + mock_get.return_value = requests_response({ + '$type': 'app.bsky.feed.defs#threadViewPost', + 'thread': THREAD_BSKY, + 'replies': [REPLY_BSKY], + }) + + self.assert_equals(POST_AUTHOR_PROFILE_AS['object'], + self.bs.get_comment(comment_id='at://id')) + mock_get.assert_called_once_with( + 'https://bsky.social/xrpc/app.bsky.feed.getPostThread?uri=at%3A%2F%2Fid&depth=1', + json=None, + headers={ + 'Authorization': 'Bearer towkin', + 'Content-Type': 'application/json', + 'User-Agent': util.user_agent, + }, + ) + From 48b264c4eb871ba7056b3176aca42610dddf7a36 Mon Sep 17 00:00:00 2001 From: Joel Auterson Date: Sat, 21 Oct 2023 15:58:12 +0100 Subject: [PATCH 06/12] Add test for activities with likes --- granary/tests/test_bluesky.py | 61 +++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/granary/tests/test_bluesky.py b/granary/tests/test_bluesky.py index d60f78cf..bbe1d671 100644 --- a/granary/tests/test_bluesky.py +++ b/granary/tests/test_bluesky.py @@ -292,6 +292,29 @@ 'size': 13, } +POST_FEED_VIEW_WITH_LIKES_BSKY = copy.deepcopy(POST_FEED_VIEW_BSKY) +POST_FEED_VIEW_WITH_LIKES_BSKY['post']['likeCount'] = 1 + +LIKE_BSKY = { + 'indexedAt': NOW.isoformat(), + 'createdAt': '2008-08-08T03:04:05', + 'actor': ACTOR_PROFILE_VIEW_BSKY +} + +POST_AUTHOR_PROFILE_WITH_LIKES_AS = copy.deepcopy(POST_AUTHOR_PROFILE_AS) +POST_AUTHOR_PROFILE_WITH_LIKES_AS['object']['tags'] = [{ + 'author': copy.deepcopy(ACTOR_AS), + 'id': 'tag:bsky.app:at://did/app.bsky.feed.post/tid_liked_by_did:web:alice.com', + 'objectType': 'activity', + 'verb': 'like', + 'url': 'https://bsky.app/profile/alice.com/post/tid', + 'object': {'url': 'https://bsky.app/profile/alice.com/post/tid'} +}] +POST_AUTHOR_PROFILE_WITH_LIKES_AS['object']['tags'][0]['author'].update({ + 'id': 'tag:bsky.app:did:web:alice.com', + 'username': 'alice.com', + 'url': 'https://bsky.app/profile/alice.com' +}) class BlueskyTest(testutil.TestCase): @@ -834,6 +857,44 @@ def test_get_activities_self_user_id(self, mock_get): }, ) + @patch('requests.get') + def test_get_activities_with_likes(self, mock_get): + mock_get.side_effect = [ + requests_response({ + 'cursor': 'timestamp::cid', + 'feed': [POST_FEED_VIEW_WITH_LIKES_BSKY], + }), + requests_response({ + 'cursor': 'timestamp::cid', + 'uri': 'at://did/app.bsky.feed.post/tid', + 'likes': [LIKE_BSKY] + }) + ] + + self.assert_equals( + [POST_AUTHOR_PROFILE_WITH_LIKES_AS], + self.bs.get_activities(fetch_likes=True) + ) + mock_get.assert_any_call( + 'https://bsky.social/xrpc/app.bsky.feed.getTimeline', + json=None, + headers={ + 'Authorization': 'Bearer towkin', + 'Content-Type': 'application/json', + 'User-Agent': util.user_agent, + }, + ) + mock_get.assert_any_call( + 'https://bsky.social/xrpc/app.bsky.feed.getLikes?uri=at%3A%2F%2Fdid%2Fapp.bsky.feed.post%2Ftid', + json=None, + headers={ + 'Authorization': 'Bearer towkin', + 'Content-Type': 'application/json', + 'User-Agent': util.user_agent, + }, + ) + + @patch('requests.get') def test_get_comment(self, mock_get): mock_get.return_value = requests_response({ From 1fb92856e4366272f302b837e970cc2ad97c804d Mon Sep 17 00:00:00 2001 From: Joel Auterson Date: Sat, 21 Oct 2023 16:32:13 +0100 Subject: [PATCH 07/12] add test for fetch_shares --- granary/tests/test_bluesky.py | 54 +++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/granary/tests/test_bluesky.py b/granary/tests/test_bluesky.py index bbe1d671..ef2a58b2 100644 --- a/granary/tests/test_bluesky.py +++ b/granary/tests/test_bluesky.py @@ -294,6 +294,8 @@ POST_FEED_VIEW_WITH_LIKES_BSKY = copy.deepcopy(POST_FEED_VIEW_BSKY) POST_FEED_VIEW_WITH_LIKES_BSKY['post']['likeCount'] = 1 +POST_FEED_VIEW_WITH_REPOSTS_BSKY = copy.deepcopy(POST_FEED_VIEW_BSKY) +POST_FEED_VIEW_WITH_REPOSTS_BSKY['post']['repostCount'] = 1 LIKE_BSKY = { 'indexedAt': NOW.isoformat(), @@ -316,6 +318,21 @@ 'url': 'https://bsky.app/profile/alice.com' }) +POST_AUTHOR_PROFILE_WITH_REPOSTS_AS = copy.deepcopy(POST_AUTHOR_PROFILE_AS) +POST_AUTHOR_PROFILE_WITH_REPOSTS_AS['object']['tags'] = [{ + 'author': copy.deepcopy(ACTOR_AS), + 'id': 'tag:bsky.app:at://did/app.bsky.feed.post/tid_reposted_by_did:web:alice.com', + 'objectType': 'activity', + 'verb': 'share', + 'url': 'https://bsky.app/profile/alice.com/post/tid', + 'object': {'url': 'https://bsky.app/profile/alice.com/post/tid'} +}] +POST_AUTHOR_PROFILE_WITH_REPOSTS_AS['object']['tags'][0]['author'].update({ + 'id': 'tag:bsky.app:did:web:alice.com', + 'username': 'alice.com', + 'url': 'https://bsky.app/profile/alice.com' +}) + class BlueskyTest(testutil.TestCase): def setUp(self): @@ -894,6 +911,43 @@ def test_get_activities_with_likes(self, mock_get): }, ) + @patch('requests.get') + def test_get_activities_with_reposts(self, mock_get): + mock_get.side_effect = [ + requests_response({ + 'cursor': 'timestamp::cid', + 'feed': [POST_FEED_VIEW_WITH_REPOSTS_BSKY], + }), + requests_response({ + 'cursor': 'timestamp::cid', + 'uri': 'at://did/app.bsky.feed.post/tid', + 'repostedBy': [ACTOR_PROFILE_VIEW_BSKY] + }) + ] + + self.assert_equals( + [POST_AUTHOR_PROFILE_WITH_REPOSTS_AS], + self.bs.get_activities(fetch_shares=True) + ) + mock_get.assert_any_call( + 'https://bsky.social/xrpc/app.bsky.feed.getTimeline', + json=None, + headers={ + 'Authorization': 'Bearer towkin', + 'Content-Type': 'application/json', + 'User-Agent': util.user_agent, + }, + ) + mock_get.assert_any_call( + 'https://bsky.social/xrpc/app.bsky.feed.getRepostedBy?uri=at%3A%2F%2Fdid%2Fapp.bsky.feed.post%2Ftid', + json=None, + headers={ + 'Authorization': 'Bearer towkin', + 'Content-Type': 'application/json', + 'User-Agent': util.user_agent, + }, + ) + @patch('requests.get') def test_get_comment(self, mock_get): From 222e4d9254d9ec99e0bdd9d53165eabd478134d6 Mon Sep 17 00:00:00 2001 From: Joel Auterson Date: Sat, 21 Oct 2023 23:41:54 +0100 Subject: [PATCH 08/12] Add test for include_shares --- granary/tests/test_bluesky.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/granary/tests/test_bluesky.py b/granary/tests/test_bluesky.py index ef2a58b2..08ecabfb 100644 --- a/granary/tests/test_bluesky.py +++ b/granary/tests/test_bluesky.py @@ -253,6 +253,12 @@ }, 'object': POST_AUTHOR_PROFILE_AS['object'], } +REPOST_PROFILE_AS = copy.deepcopy(REPOST_AS) +REPOST_PROFILE_AS['actor'].update({ + 'username': 'bob.com', + 'url': 'https://bsky.app/profile/bob.com', +}) + REPOST_BSKY = { '$type': 'app.bsky.feed.repost', 'subject': { @@ -948,6 +954,18 @@ def test_get_activities_with_reposts(self, mock_get): }, ) + @patch('requests.get') + def test_get_activities_include_shares(self, mock_get): + mock_get.return_value = requests_response({ + 'cursor': 'timestamp::cid', + 'feed': [POST_FEED_VIEW_BSKY, REPOST_BSKY_FEED_VIEW_POST], + }) + + self.assert_equals( + [POST_AUTHOR_PROFILE_AS, REPOST_PROFILE_AS], + self.bs.get_activities(include_shares=True) + ) + @patch('requests.get') def test_get_comment(self, mock_get): From 30c7294e07b5eec3df42b352fe927dcef7351e17 Mon Sep 17 00:00:00 2001 From: Joel Auterson Date: Sun, 22 Oct 2023 17:25:15 +0100 Subject: [PATCH 09/12] Add test for replies --- granary/tests/test_bluesky.py | 59 ++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/granary/tests/test_bluesky.py b/granary/tests/test_bluesky.py index 08ecabfb..10e56ef5 100644 --- a/granary/tests/test_bluesky.py +++ b/granary/tests/test_bluesky.py @@ -284,11 +284,30 @@ } THREAD_AS = copy.deepcopy(POST_AS) -THREAD_AS['object']['replies'] = [REPLY_AS['object']] +THREAD_REPLY_AS = copy.deepcopy(REPLY_AS['object']) +THREAD_REPLY_AS['id'] = 'tag:bsky.app:at://did/app.bsky.feed.post/tid' +THREAD_AS['object']['replies'] = {'items': [THREAD_REPLY_AS]} +THREAD_AS['object']['author'] = ACTOR_AS +THREAD_AS['object']['author'].update({ + 'username': 'alice.com', + 'url': 'https://bsky.app/profile/alice.com', +}) +THREAD_AS['object']['url'] = 'https://bsky.app/profile/alice.com/post/tid' +THREAD_AS['actor'].update({ + 'username': 'alice.com', + 'url': 'https://bsky.app/profile/alice.com', +}) + +THREAD_REPLY_BSKY = { + '$type': 'app.bsky.feed.defs#threadViewPost', + 'post': copy.deepcopy(REPLY_POST_VIEW_BSKY), + 'replies': [], +} + THREAD_BSKY = { '$type': 'app.bsky.feed.defs#threadViewPost', 'post': POST_AUTHOR_BSKY, - 'replies': [REPLY_BSKY], + 'replies': [THREAD_REPLY_BSKY], } BLOB = { @@ -302,6 +321,8 @@ POST_FEED_VIEW_WITH_LIKES_BSKY['post']['likeCount'] = 1 POST_FEED_VIEW_WITH_REPOSTS_BSKY = copy.deepcopy(POST_FEED_VIEW_BSKY) POST_FEED_VIEW_WITH_REPOSTS_BSKY['post']['repostCount'] = 1 +POST_FEED_VIEW_WITH_REPLIES_BSKY = copy.deepcopy(POST_FEED_VIEW_BSKY) +POST_FEED_VIEW_WITH_REPLIES_BSKY['post']['replyCount'] = 1 LIKE_BSKY = { 'indexedAt': NOW.isoformat(), @@ -840,9 +861,7 @@ def test_get_activities_friends(self, mock_get): @patch('requests.get') def test_get_activities_activity_id(self, mock_get): mock_get.return_value = requests_response({ - '$type': 'app.bsky.feed.defs#threadViewPost', 'thread': THREAD_BSKY, - 'replies': [REPLY_BSKY], }) self.assert_equals([POST_AUTHOR_PROFILE_AS], @@ -966,6 +985,38 @@ def test_get_activities_include_shares(self, mock_get): self.bs.get_activities(include_shares=True) ) + @patch('requests.get') + def test_get_activities_with_replies(self, mock_get): + mock_get.side_effect = [ + requests_response({ + 'cursor': 'timestamp::cid', + 'feed': [POST_FEED_VIEW_WITH_REPLIES_BSKY], + }), + requests_response({ + 'thread': THREAD_BSKY, + }) + ] + + self.assert_equals([THREAD_AS], + self.bs.get_activities(fetch_replies=True)) + mock_get.assert_any_call( + 'https://bsky.social/xrpc/app.bsky.feed.getTimeline', + json=None, + headers={ + 'Authorization': 'Bearer towkin', + 'Content-Type': 'application/json', + 'User-Agent': util.user_agent, + }, + ) + mock_get.assert_any_call( + 'https://bsky.social/xrpc/app.bsky.feed.getPostThread?uri=at%3A%2F%2Fdid%2Fapp.bsky.feed.post%2Ftid', + json=None, + headers={ + 'Authorization': 'Bearer towkin', + 'Content-Type': 'application/json', + 'User-Agent': util.user_agent, + }, + ) @patch('requests.get') def test_get_comment(self, mock_get): From c3f58a73fa63ffc5a7e1626b9ecf44b4cfdf40b1 Mon Sep 17 00:00:00 2001 From: Joel Auterson Date: Sun, 22 Oct 2023 17:50:20 +0100 Subject: [PATCH 10/12] Update replies test to include recursive replies --- granary/tests/test_bluesky.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/granary/tests/test_bluesky.py b/granary/tests/test_bluesky.py index 10e56ef5..847d0d3b 100644 --- a/granary/tests/test_bluesky.py +++ b/granary/tests/test_bluesky.py @@ -286,7 +286,14 @@ THREAD_AS = copy.deepcopy(POST_AS) THREAD_REPLY_AS = copy.deepcopy(REPLY_AS['object']) THREAD_REPLY_AS['id'] = 'tag:bsky.app:at://did/app.bsky.feed.post/tid' -THREAD_AS['object']['replies'] = {'items': [THREAD_REPLY_AS]} +THREAD_REPLY2_AS = copy.deepcopy(REPLY_AS['object']) +THREAD_REPLY2_AS['id'] = 'tag:bsky.app:at://did/app.bsky.feed.post/tid2' +THREAD_REPLY2_AS['url'] = 'https://bsky.app/profile/did/post/tid2' +THREAD_REPLY2_AS['inReplyTo'] = [{ + 'id': 'at://did/app.bsky.feed.post/tid', + 'url': 'https://bsky.app/profile/did/post/tid' +}] +THREAD_AS['object']['replies'] = {'items': [THREAD_REPLY_AS, THREAD_REPLY2_AS]} THREAD_AS['object']['author'] = ACTOR_AS THREAD_AS['object']['author'].update({ 'username': 'alice.com', @@ -298,17 +305,29 @@ 'url': 'https://bsky.app/profile/alice.com', }) -THREAD_REPLY_BSKY = { - '$type': 'app.bsky.feed.defs#threadViewPost', - 'post': copy.deepcopy(REPLY_POST_VIEW_BSKY), - 'replies': [], -} - THREAD_BSKY = { '$type': 'app.bsky.feed.defs#threadViewPost', 'post': POST_AUTHOR_BSKY, - 'replies': [THREAD_REPLY_BSKY], + 'replies': [{ + '$type': 'app.bsky.feed.defs#threadViewPost', + 'post': copy.deepcopy(REPLY_POST_VIEW_BSKY), + 'replies': [{ + '$type': 'app.bsky.feed.defs#threadViewPost', + 'post': copy.deepcopy(REPLY_POST_VIEW_BSKY), + 'replies': [], + }], + }], } +THREAD_BSKY['replies'][0]['replies'][0]['post'].update({ + 'uri': 'at://did/app.bsky.feed.post/tid2' +}) +THREAD_BSKY['replies'][0]['replies'][0]['post']['record']['reply'].update({ + 'parent': { + '$type': 'com.atproto.repo.strongRef', + 'uri': 'at://did/app.bsky.feed.post/tid', + 'cid': 'TODO' + } +}) BLOB = { '$type': 'blob', From 4d4ba6d31e25ec09fd8956f42fcc1d6317824ada Mon Sep 17 00:00:00 2001 From: Joel Auterson Date: Mon, 23 Oct 2023 08:30:29 +0100 Subject: [PATCH 11/12] Remove some redundant stuff --- granary/tests/test_bluesky.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/granary/tests/test_bluesky.py b/granary/tests/test_bluesky.py index 847d0d3b..afe30f5b 100644 --- a/granary/tests/test_bluesky.py +++ b/granary/tests/test_bluesky.py @@ -58,10 +58,6 @@ 'description': 'hi there', } -POST_OBJ_AS = { - -} - POST_AS = { 'objectType': 'activity', 'id': 'at://did/app.bsky.feed.post/tid', @@ -1040,9 +1036,7 @@ def test_get_activities_with_replies(self, mock_get): @patch('requests.get') def test_get_comment(self, mock_get): mock_get.return_value = requests_response({ - '$type': 'app.bsky.feed.defs#threadViewPost', 'thread': THREAD_BSKY, - 'replies': [REPLY_BSKY], }) self.assert_equals(POST_AUTHOR_PROFILE_AS['object'], From cbab12e24f9c12f2bc71b6ae77ccb2e0f916cae9 Mon Sep 17 00:00:00 2001 From: Joel Auterson Date: Mon, 23 Oct 2023 17:14:30 +0100 Subject: [PATCH 12/12] Drop context field --- granary/bluesky.py | 1 - 1 file changed, 1 deletion(-) diff --git a/granary/bluesky.py b/granary/bluesky.py index 15559aeb..9c0c024d 100644 --- a/granary/bluesky.py +++ b/granary/bluesky.py @@ -1077,7 +1077,6 @@ def _post_to_activity(self, post): 'actor': obj.get('author'), 'object': obj, 'objectType': 'activity', - 'context': {'inReplyTo': obj.get('inReplyTo')}, } def _make_like(self, post, actor):