Skip to content

Commit

Permalink
start on bluesky!
Browse files Browse the repository at this point in the history
  • Loading branch information
snarfed committed Dec 28, 2022
1 parent c3790f3 commit ecb8f0d
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ _Breaking changes:_

_Non-breaking changes:_

* Add new `bluesky` module for [Bluesky](https://blueskyweb.org/)/[AT Protocol](https://atproto.com/).
* `atom`
* Bug fix for rendering image attachments without `image` field to Atom.
* `jsonfeed`:
Expand Down
128 changes: 128 additions & 0 deletions granary/bluesky.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""Bluesky source class.
https://bsky.app/
https://atproto.com/lexicons/app-bsky-actor
https://github.com/bluesky-social/atproto/tree/main/lexicons/app/bsky
"""
import logging

from oauth_dropins.webutil import util


def from_as1(obj):
"""Converts an AS1 object to a Bluesky object.
The objectType field is required.
Args:
profile: dict, AS1 object
Returns: dict, app.bsky.* object
Raises:
ValueError
if the objectType field is missing or unsupported
"""
type = obj.get('objectType')
if not type:
raise ValueError('AS1 object missing objectType field')

# TODO: once we're on Python 3.10, switch this to a match statement!
if type == 'person':
ret = {
'$type': 'app.bsky.actor.profile',
'displayName': obj.get('displayName'),
'description': obj.get('summary'),
'avatar': util.get_url(obj, 'image'),
}

elif type in ('article', 'mention', 'note'):
entities = []
for tag in util.get_list(obj, 'tags'):
url = tag.get('url')
if url:
try:
start = int(tag.get('startIndex'))
end = start + int(tag.get('length'))
except ValueError:
start = end = None
entities.append({
'type': 'link',
'value': url,
'index': {
'start': start,
'end': end,
},
})

ret = {
'$type': 'app.bsky.feed.post',
'text': obj.get('content'),
'createdAt': obj.get('published'),
'embed': {
'images': util.get_urls(obj, 'image'),
},
'entities': entities,
}

elif type == 'share':
ret = {
'$type': 'app.bsky.',
}

elif type == 'follow':
ret = {
'$type': 'app.bsky.',
}

else:
raise ValueError(f'AS1 object has unknown objectType: {type}')

return util.trim_nulls(ret)


def to_as1(obj):
"""Converts a Bluesky object to an AS1 object.
The $type field is required.
Args:
profile: dict, app.bsky.* object
Returns: dict, AS1 object
Raises:
ValueError
if the $type field is missing or unsupported
"""
type = obj.get('$type')
if not type:
raise ValueError('Bluesky object missing $type field')

# TODO: once we're on Python 3.10, switch this to a match statement!
if type == 'app.bsky.actor.profile':
return {
}
elif type == 'app.bsky.feed.post':
return {
}
elif type == 'app.bsky.feed.repost':
return {
}
elif type == 'app.bsky.graph.follow':
return {
}

raise ValueError(f'Bluesky object has unknown $type: {type}')


# class Bluesky(source.Source):
# """Bluesky source class. See file docstring and Source class for details."""

# DOMAIN = 'bsky.app'
# BASE_URL = 'https://bsky.app'
# NAME = 'Bluesky'
# # OPTIMIZED_COMMENTS = None # TODO

# def __init__(self):
# pass
74 changes: 74 additions & 0 deletions granary/tests/test_bluesky.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Unit tests for jsonfeed.py."""
from oauth_dropins.webutil import testutil

from ..bluesky import from_as1, to_as1

# app.bsky.actor.profile
# /Users/ryan/src/atproto/lexicons/app/bsky/actor/profile.json
# https://github.com/bluesky-social/atproto/blob/main/packages/pds/tests/crud.test.ts#L211-L217
# {
# displayName: 'alice',
# createdAt: new Date().toISOString(),
# },

# app.bsky.feed.post
# /Users/ryan/src/atproto/lexicons/app/bsky/feed/post.json
# https://github.com/bluesky-social/atproto/blob/main/packages/pds/tests/crud.test.ts#L74-L82
# record: {
# $type: 'app.bsky.feed.post',
# text: 'Hello, world!',
# createdAt: new Date().toISOString(),
# },

# app.bsky.feed.repost
# /Users/ryan/src/atproto/lexicons/app/bsky/feed/repost.json
# https://github.com/bluesky-social/atproto/blob/main/packages/pds/tests/seeds/client.ts#L294-L298
# { subject: subject.raw, createdAt: new Date().toISOString() },


# app.bsky.graph.follow
# /Users/ryan/src/atproto/lexicons/app/bsky/graph/follow.json
# https://github.com/bluesky-social/atproto/blob/main/packages/pds/tests/seeds/client.ts#L183-L190
# {
# subject: to.raw,
# createdAt: new Date().toISOString(),
# },

# # link/other embed (no test)
# app.bsky.embed.external
# /Users/ryan/src/atproto/lexicons/app/bsky/embed/external.json

# # image
# app.bsky.embed.images
# /Users/ryan/src/atproto/lexicons/app/bsky/embed/images.json
# https://github.com/bluesky-social/atproto/blob/main/packages/pds/tests/crud.test.ts#L178-L191
# {
# $type: 'app.bsky.feed.post',
# text: "Here's a key!",
# createdAt: new Date().toISOString(),
# embed: {
# $type: 'app.bsky.embed.images',
# images: [
# { image: { cid: image.cid, mimeType: 'image/jpeg' }, alt: '' },
# ],
# },
# },


class TestBluesky(testutil.TestCase):

def test_to_as1_missing_objectType(self):
with self.assertRaises(ValueError):
to_as1({'foo': 'bar'})

def test_to_as1_unknown_objectType(self):
with self.assertRaises(ValueError):
to_as1({'objectType': 'poll'})

def test_to_as1_missing_type(self):
with self.assertRaises(ValueError):
to_as1({'foo': 'bar'})

def test_to_as1_unknown_type(self):
with self.assertRaises(ValueError):
to_as1({'$type': 'app.bsky.foo'})
3 changes: 2 additions & 1 deletion granary/tests/test_testdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from oauth_dropins.webutil import util
from oauth_dropins.webutil.util import json_dumps, json_loads

from .. import as2, jsonfeed, microformats2, rss
from .. import as2, bluesky, jsonfeed, microformats2, rss

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -117,6 +117,7 @@ def rss_to_objects(feed):
('as2.json', ['as-from-as2.json', 'as.json'], as2.to_as1, ()),
('as.json', ['rss.xml'], rss_from_activities, ()),
('rss.xml', ['as-from-rss.json', 'as.json'], rss_to_objects, ()),
('as.json', ['bsky.json'], bluesky.from_as1, ()),
)

test_funcs = {}
Expand Down
6 changes: 6 additions & 0 deletions granary/tests/testdata/actor.bsky.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$type": "app.bsky.actor.profile",
"displayName": "Martin Smith",
"description": "this is my bio",
"avatar": "http://example.com/martin/image"
}
5 changes: 5 additions & 0 deletions granary/tests/testdata/follow.bsky.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"$type": "app.bsky.graph.follow",
"subject": "http://follower",
"createdAt": "2012-02-22T20:26:41"
}
16 changes: 16 additions & 0 deletions granary/tests/testdata/note.bsky.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$type": "app.bsky.feed.post",
"text": "A note. link too",
"createdAt": "2012-02-22T20:26:41",
"embed": {
"images": ["http://example.com/blog-post-123/image"]
},
"entities": [{
"type": "link",
"value": "http://my/link",
"index": {
"start": 8,
"end": 12
}
}]
}
5 changes: 5 additions & 0 deletions granary/tests/testdata/repost.bsky.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"$type": "app.bsky.feed.repost",
"subject": "http://example.com/alice",
"createdAt": "2012-02-22T20:26:41"
}

0 comments on commit ecb8f0d

Please sign in to comment.