From 547e0630ad960ee57563589cae562c6cd566597d Mon Sep 17 00:00:00 2001 From: Ryan Barrett Date: Fri, 29 Nov 2024 22:42:05 -0800 Subject: [PATCH] ATProto.send: fetch blobs for attachment images for #1411 --- atproto.py | 31 +++++++++++++++++-------- tests/test_atproto.py | 54 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 9 deletions(-) diff --git a/atproto.py b/atproto.py index 3af4f6b8..ff58ce23 100644 --- a/atproto.py +++ b/atproto.py @@ -822,6 +822,14 @@ def _convert(cls, obj, fetch_blobs=False, from_user=None): if not obj.as1: return {} + obj_as1 = obj.as1 + + # generate link preview attachment for first link in content, if any + Source.postprocess_object( + (as1.get_object(obj_as1) if obj_as1.get('objectType') == 'activity' + else obj_as1), + first_link_to_attachment=True) + blobs = {} # maps str URL to dict blob object if fetch_blobs: def fetch_blob(url, blob_field, name, check_size=True, check_type=True): @@ -836,23 +844,28 @@ def fetch_blob(url, blob_field, name, check_size=True, check_type=True): except (RequestException, ValidationError) as e: logger.info(f'failed, skipping {url} : {e}') - for o in obj.as1, as1.get_object(obj.as1): + for o in obj_as1, as1.get_object(obj_as1): for url in util.get_urls(o, 'image'): # TODO: maybe eventually check size and type? the current # 1MB limit feels too small though, and the AppView doesn't # seem to validate, it's happily allowing bigger image blobs # and different types as of 9/29/2024: # https://github.com/snarfed/bridgy-fed/issues/1348#issuecomment-2381056468 - fetch_blob(url, appview.defs['app.bsky.embed.images#image']['properties'], - name='image', check_size=False, check_type=False) + props = appview.defs['app.bsky.embed.images#image']['properties'] + fetch_blob(url, props, name='image', check_size=False, + check_type=False) for att in util.get_list(o, 'attachments'): if isinstance(att, dict): - fetch_blob(att.get('stream', {}).get('url'), - appview.defs['app.bsky.embed.video']['properties'], + props = appview.defs['app.bsky.embed.video']['properties'] + fetch_blob(att.get('stream', {}).get('url'), props, name='video', check_size=True, check_type=True) + for url in util.get_urls(att, 'image'): + props = appview.defs['app.bsky.embed.external#external']['properties'] + fetch_blob(url, props, name='thumb', + check_size=False, check_type=False) - inner_obj = as1.get_object(obj.as1) or obj.as1 + inner_obj = as1.get_object(obj_as1) or obj_as1 orig_url = as1.get_url(inner_obj) or inner_obj.get('id') # convert! using our records in the datastore and fetching code instead @@ -860,9 +873,9 @@ def fetch_blob(url, blob_field, name, check_size=True, check_type=True): client = DatastoreClient(f'https://{os.environ["APPVIEW_HOST"]}') as_embed = obj.atom or obj.rss try: - ret = bluesky.from_as1(cls.translate_ids(obj.as1), blobs=blobs, + ret = bluesky.from_as1(cls.translate_ids(obj_as1), blobs=blobs, client=client, original_fields_prefix='bridgy', - as_embed=as_embed, first_link_embed=True) + as_embed=as_embed) except (ValueError, RequestException): logger.info(f"Couldn't convert to ATProto", exc_info=True) return {} @@ -870,7 +883,7 @@ def fetch_blob(url, blob_field, name, check_size=True, check_type=True): if from_proto != ATProto: if ret['$type'] == 'app.bsky.actor.profile': # populated by Protocol.convert - if orig_summary := obj.as1.get('bridgyOriginalSummary'): + if orig_summary := obj_as1.get('bridgyOriginalSummary'): ret['bridgyOriginalDescription'] = orig_summary else: # don't use granary's since it will include source links diff --git a/tests/test_atproto.py b/tests/test_atproto.py index fbf7df8b..a7a2c0b2 100644 --- a/tests/test_atproto.py +++ b/tests/test_atproto.py @@ -1475,6 +1475,60 @@ def test_send_note_existing_repo(self, mock_create_task): mock_create_task.assert_called() # atproto-commit + @patch.object(tasks_client, 'create_task', return_value=Task(name='my task')) + @patch('requests.get', side_effect=[ + requests_response(f"""\ + + + A poast + + + + +""", url='http://orig/inal'), + requests_response('blob contents', content_type='image/png'), + ]) + def test_send_note_first_link_to_attachment(self, _, __): + user = self.make_user_and_repo() + + obj = Object(id='fake:post', source_protocol='fake', our_as1={ + **NOTE_AS, + 'content': 'My original post', + }) + self.assertTrue(ATProto.send(obj, 'https://bsky.brid.gy')) + + # check repo, record + did = user.key.get().get_copy(ATProto) + repo = self.storage.load_repo(did) + last_tid = arroba.util.int_to_tid(arroba.util._tid_ts_last) + self.assertEqual({ + **NOTE_BSKY, + 'bridgyOriginalText': 'My original post', + 'embed': { + '$type': 'app.bsky.embed.external', + 'external': { + '$type': 'app.bsky.embed.external#external', + 'description': 'Descrypshun', + 'title': 'Titull', + 'uri': 'http://orig/inal', + 'thumb': { + '$type': 'blob', + 'mimeType': 'image/png', + 'ref': BLOB_CID, + 'size': 13, + }, + }, + }, + 'facets': [{ + '$type': 'app.bsky.richtext.facet', + 'index': {'byteStart': 3, 'byteEnd': 11}, + 'features': [{ + '$type': 'app.bsky.richtext.facet#link', + 'uri': 'http://orig/inal', + }], + }], + }, repo.get_record('app.bsky.feed.post', last_tid)) + @patch.object(tasks_client, 'create_task', return_value=Task(name='my task')) def test_send_update_note(self, mock_create_task): self.test_send_note_existing_repo()