Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add image aspect ratio for Bluesky #844

Merged
merged 7 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions granary/bluesky.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
from oauth_dropins.webutil import util
from oauth_dropins.webutil.util import trim_nulls
import requests
from PIL import Image
from io import BytesIO

from . import as1
from .as2 import QUOTE_RE_SUFFIX
Expand Down Expand Up @@ -422,7 +424,7 @@ def base_object(obj):
return {}


def from_as1(obj, out_type=None, blobs=None, client=None,
def from_as1(obj, out_type=None, blobs=None, aspects=None, client=None,
original_fields_prefix=None, as_embed=False):
"""Converts an AS1 object to a Bluesky object.

Expand All @@ -443,6 +445,8 @@ def from_as1(obj, out_type=None, blobs=None, client=None,
``ref``, ``mimeType``, and ``size``) to use in the returned object. If not
provided, or if this doesn't have an ``image`` or similar URL in the input
object, its output blob will be omitted.
aspects (dict): optional mapping from str URL to int (width,height) tuple.
Used to provide aspect ratio in image embeds.
client (Bluesky or lexrpc.Client): optional; if provided, this will be used
to make API calls to PDSes to fetch and populate CIDs for records
referenced by replies, likes, reposts, etc.
Expand Down Expand Up @@ -474,6 +478,8 @@ def from_as1(obj, out_type=None, blobs=None, client=None,
actor = as1.get_object(activity, 'actor')
if blobs is None:
blobs = {}
if aspects is None:
aspects = {}

# validate out_type
if out_type:
Expand Down Expand Up @@ -652,11 +658,17 @@ def from_as1(obj, out_type=None, blobs=None, client=None,
'$type': 'app.bsky.embed.images',
'images': [],
}
images_record_embed['images'].append({
image_record = {
'$type': 'app.bsky.embed.images#image',
'image': blob,
'alt': alt,
})
}
if aspect := aspects.get(url):
image_record['aspectRatio'] = {
'width': aspect[0],
'height': aspect[1]
}
images_record_embed['images'].append(image_record)

# first video => embed
attachments = util.get_list(obj, 'attachments')
Expand Down Expand Up @@ -2172,8 +2184,8 @@ def _create(self, obj, preview=None, include_link=OMIT_LINK, ignore_formatting=F
description=preview_description)

else:
blobs = self.upload_media(images)
post_atp = from_as1(obj, blobs=blobs, client=self)
blobs, aspects = self.upload_images(images)
post_atp = from_as1(obj, blobs=blobs, aspects=aspects, client=self)
post_atp['text'] = content

# facet for link to original post, if any
Expand Down Expand Up @@ -2247,24 +2259,30 @@ def preview_delete(self, at_uri):
def base_object(self, obj):
return base_object(obj)

def upload_media(self, media):
def upload_images(self, images):
snarfed marked this conversation as resolved.
Show resolved Hide resolved
blobs = {}
aspects = {}

for obj in media:
for obj in images:
url = util.get_url(obj, key='stream') or util.get_url(obj)
if not url or url in blobs:
continue

with util.requests_get(url, stream=True) as fetch:
fetch.raise_for_status()
# 1,000,000 bytes is the upper limit for a single image on Bluesky
image_data = BytesIO(util.FileLimiter(fetch.raw, 1_000_000).read())
snarfed marked this conversation as resolved.
Show resolved Hide resolved
image = Image.open(image_data)
aspects[url] = image.size
image_data.seek(0)
upload = self.client.com.atproto.repo.uploadBlob(
input=fetch.raw,
input=image_data,
headers={'Content-Type': fetch.headers['Content-Type']}
)

blobs[url] = upload['blob']

return blobs
return blobs, aspects

def truncate(self, *args, type=None, **kwargs):
"""Thin wrapper around :meth:`Source.truncate` that sets default kwargs."""
Expand Down
13 changes: 10 additions & 3 deletions granary/tests/test_bluesky.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Most tests are via files in testdata/.
"""
import copy
import os
from io import BytesIO
from unittest import skip
from unittest.mock import ANY, patch
Expand Down Expand Up @@ -3481,8 +3482,10 @@ def test_preview_with_media(self):
@patch('requests.post')
@patch('requests.get')
def test_create_with_media(self, mock_get, mock_post):
mock_get.return_value = requests_response(
'pic data', headers={'Content-Type': 'my/pic'})
image_path = os.path.join(os.path.dirname(__file__), 'testdata/image.jpg')
with open(image_path, 'rb') as f:
mock_get.return_value = requests_response(
f.read(), headers={'Content-Type': 'image/jpeg'})

at_uri = 'at://did:plc:me/app.bsky.feed.post/abc123'
mock_post.side_effect = [
Expand All @@ -3503,7 +3506,7 @@ def test_create_with_media(self, mock_get, mock_post):
data=ANY,
headers={
'Authorization': 'Bearer towkin',
'Content-Type': 'my/pic',
'Content-Type': 'image/jpeg',
'User-Agent': util.user_agent,
})
# lexrpc.Client passes a BytesIO as data. sadly requests reads from that
Expand All @@ -3513,6 +3516,10 @@ def test_create_with_media(self, mock_get, mock_post):
expected = copy.deepcopy(POST_BSKY_IMAGES)
del expected['fooOriginalText']
del expected['fooOriginalUrl']
expected['embed']['images'][0]['aspectRatio'] = {
'width': 500,
snarfed marked this conversation as resolved.
Show resolved Hide resolved
'height': 500
}
self.assert_call(mock_post, 'com.atproto.repo.createRecord', json={
'repo': self.bs.did,
'collection': 'app.bsky.feed.post',
Expand Down
Binary file added granary/tests/testdata/image.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ multiformats==0.3.1.post4
multiformats-config==0.3.1
oauthlib==3.2.2
packaging==24.2
Pillow==11.0.0
snarfed marked this conversation as resolved.
Show resolved Hide resolved
pkce==1.0.3
praw==7.8.1
prawcore==2.4.0
Expand Down