Porównaj commity

...

10 Commity

Autor SHA1 Wiadomość Data
dependabot[bot] ca390a6ac7 build(deps): bump typing-validation from 1.2.10.post4 to 1.2.11.post1
Bumps [typing-validation](https://github.com/hashberg-io/typing-validation) from 1.2.10.post4 to 1.2.11.post1.
- [Release notes](https://github.com/hashberg-io/typing-validation/releases)
- [Commits](https://github.com/hashberg-io/typing-validation/commits)

---
updated-dependencies:
- dependency-name: typing-validation
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-18 05:23:54 -07:00
dependabot[bot] e37c2b2b9a build(deps): bump google-cloud-logging from 3.9.0 to 3.10.0
Bumps [google-cloud-logging](https://github.com/googleapis/python-logging) from 3.9.0 to 3.10.0.
- [Release notes](https://github.com/googleapis/python-logging/releases)
- [Changelog](https://github.com/googleapis/python-logging/blob/main/CHANGELOG.md)
- [Commits](https://github.com/googleapis/python-logging/compare/v3.9.0...v3.10.0)

---
updated-dependencies:
- dependency-name: google-cloud-logging
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-15 06:02:47 -07:00
Ryan Barrett 048cd17ba5
bug fix for pds_for change in e86763f: don't use Object.as1 since that infinite loops 2024-03-14 21:16:09 -07:00
Ryan Barrett e86763f654
Object.as1 from bsky: bug fix for PDS in getBlob image URLs 2024-03-14 20:26:46 -07:00
Ryan Barrett 878bbde328
atproto: handle bsky.app URLs in load, not fetch 2024-03-14 15:40:25 -07:00
Ryan Barrett 9b2bb9ef37
make ATProto.load([DID]) return the profile record by default
add did_doc kwarg to make it return the DID doc instead
2024-03-13 16:08:08 -07:00
Ryan Barrett 5eac0e06d0
deps: upgrade google-api-core to 2.17.1 2024-03-13 16:07:09 -07:00
Ryan Barrett 69e4b039e9
atproto: update authed_as, actor authz checks in poll_* 2024-03-13 15:41:03 -07:00
Ryan Barrett bad7052663
atproto: add delete support 2024-03-13 14:47:48 -07:00
Ryan Barrett d667c8ece4
atproto: add update support 2024-03-13 14:40:31 -07:00
7 zmienionych plików z 240 dodań i 78 usunięć

Wyświetl plik

@ -81,7 +81,7 @@ class ATProto(User, Protocol):
@ndb.ComputedProperty
def handle(self):
"""Returns handle if the DID document includes one, otherwise None."""
if did_obj := ATProto.load(self.key.id()):
if did_obj := ATProto.load(self.key.id(), did_doc=True):
if aka := util.get_first(did_obj.raw, 'alsoKnownAs', ''):
handle, _, _ = parse_at_uri(aka)
if handle:
@ -114,8 +114,13 @@ class ATProto(User, Protocol):
return did.resolve_handle(handle, get_fn=util.requests_get)
@staticmethod
def profile_at_uri(id):
assert id.startswith('did:')
return f'at://{id}/app.bsky.actor.profile/self'
def profile_id(self):
return f'at://{self.key.id()}/app.bsky.actor.profile/self'
return self.profile_at_uri(self.key.id())
@classmethod
def target_for(cls, obj, shared=False):
@ -166,14 +171,16 @@ class ATProto(User, Protocol):
else:
return None
did_obj = ATProto.load(repo)
did_obj = ATProto.load(repo, did_doc=True)
if did_obj:
return cls.pds_for(did_obj)
# TODO: what should we do if the DID doesn't exist? should we return
# None here? or do we need this path to return BF's URL so that we
# then create the DID for non-ATP users on demand?
if obj.as1:
# don't use Object.as1 if bsky is set, since that conversion calls
# pds_for, which would infinite loop
if not obj.bsky and obj.as1:
if owner := as1.get_owner(obj.as1):
if user_key := Protocol.key_for(owner):
if user := user_key.get():
@ -278,23 +285,24 @@ class ATProto(User, Protocol):
logger.info(f'Skipping sending {verb}, not supported in ATProto')
return False
# convert to Bluesky record; short circuits on error
try:
record = to_cls.convert(obj, fetch_blobs=True)
except ValueError as e:
logger.info(f'Skipping due to {e}')
return False
# determine "base" object, if any
type = as1.object_type(obj.as1)
base_obj = obj
if type in ('post', 'update', 'delete'):
obj_as1 = as1.get_object(obj.as1)
type = as1.object_type(obj_as1)
# TODO: should we not load for deletes?
base_obj = PROTOCOLS[obj.source_protocol].load(obj_as1['id'])
if not base_obj:
base_obj = obj
# convert to Bluesky record; short circuits on error
try:
record = to_cls.convert(base_obj, fetch_blobs=True)
except ValueError as e:
logger.info(f'Skipping due to {e}')
return False
# find user
from_cls = PROTOCOLS[obj.source_protocol]
from_key = from_cls.actor_key(obj)
@ -307,7 +315,7 @@ class ATProto(User, Protocol):
did = user.get_copy(ATProto)
assert did
logger.info(f'{user.key} is {did}')
did_doc = to_cls.load(did)
did_doc = to_cls.load(did, did_doc=True)
pds = to_cls.pds_for(did_doc)
if not pds or util.domain_from_link(pds) not in DOMAINS:
logger.warning(f'{from_key} {did} PDS {pds} is not us')
@ -325,20 +333,55 @@ class ATProto(User, Protocol):
ndb.transactional()
def write():
tid = next_tid()
logger.info(f'Storing ATProto {type} {tid}: ' +
match verb:
case 'update':
action = Action.UPDATE
case 'delete':
action = Action.DELETE
case _:
action = Action.CREATE
rkey = next_tid()
if verb in ('update', 'delete'):
# load existing record, check that it's the same one
copy = base_obj.get_copy(to_cls)
assert copy
copy_did, coll, rkey = parse_at_uri(copy)
assert copy_did == did
assert coll == type
logger.info(f'Storing ATProto {action} {type} {rkey}: ' +
json_dumps(dag_json.encode(record).decode(), indent=2))
repo.apply_writes([Write(action=action, collection=type, rkey=rkey,
record=record)])
repo.apply_writes([Write(action=Action.CREATE, collection=type,
rkey=tid, record=record)])
at_uri = f'at://{did}/{type}/{tid}'
at_uri = f'at://{did}/{type}/{rkey}'
base_obj.add('copies', Target(uri=at_uri, protocol=to_cls.LABEL))
base_obj.put()
write()
return True
@classmethod
def load(cls, id, did_doc=False, **kwargs):
"""Thin wrapper that converts DIDs and bsky.app URLs to at:// URIs.
Args:
did_doc (bool): if True, loads and returns a DID document object
instead of an ``app.bsky.actor.profile/self``.
"""
if id.startswith('did:') and not did_doc:
id = cls.profile_at_uri(id)
elif id.startswith('https://bsky.app/'):
try:
id = bluesky.web_url_to_at_uri(id)
except ValueError as e:
logger.warning(f"Couldn't convert {id} to at:// URI: {e}")
return None
return super().load(id, **kwargs)
@classmethod
def fetch(cls, obj, **kwargs):
"""Tries to fetch a ATProto object.
@ -360,6 +403,8 @@ class ATProto(User, Protocol):
logger.info(f"ATProto can't fetch {id}")
return False
assert not id.startswith('https://bsky.app/') # handled in load
# did:plc, did:web
if id.startswith('did:'):
try:
@ -369,15 +414,6 @@ class ATProto(User, Protocol):
util.interpret_http_exception(e)
return False
if id.startswith('https://bsky.app/'):
try:
id = bluesky.web_url_to_at_uri(id)
except ValueError:
return False
if not id:
return False
obj.key = ndb.Key(Object, id)
# at:// URI. if it has a handle, resolve and replace with DID.
# examples:
# at://did:plc:s2koow7r6t7tozgd4slc3dsg/app.bsky.feed.post/3jqcpv7bv2c2q
@ -502,20 +538,22 @@ def poll_notifications():
privkey=repo.signing_key)
resp = client.app.bsky.notification.listNotifications()
for notif in resp['notifications']:
actor_did = notif['author']['did']
logger.debug(f'Got {notif["reason"]} from {notif["author"]["handle"]} {notif["uri"]} {notif["cid"]} : {json_dumps(notif, indent=2)}')
# TODO: verify sig. skipping this for now because we're getting
# these from the AppView, which is trusted, specifically we expect
# the BGS and/or the AppView already checked sigs.
obj = Object.get_or_create(id=notif['uri'], bsky=notif['record'],
source_protocol=ATProto.ABBREV)
source_protocol=ATProto.ABBREV,
actor=actor_did)
if not obj.status:
obj.status = 'new'
obj.add('notify', user.key)
obj.put()
common.create_task(queue='receive', obj=obj.key.urlsafe(),
authed_as=notif['author']['did'])
authed_as=actor_did)
return 'OK'
@ -569,13 +607,16 @@ def poll_posts():
# TODO: verify sig. skipping this for now because we're getting
# these from the AppView, which is trusted, specifically we expect
# the BGS and/or the AppView already checked sigs.
author_did = post['author']['did']
obj = Object.get_or_create(id=post['uri'], bsky=post['record'],
source_protocol=ATProto.ABBREV)
source_protocol=ATProto.ABBREV,
actor=author_did)
if not obj.status:
obj.status = 'new'
obj.add('feed', user.key)
obj.put()
common.create_task(queue='receive', obj=obj.key.urlsafe(), authed_as=did)
common.create_task(queue='receive', obj=obj.key.urlsafe(),
authed_as=author_did)
return 'OK'

Wyświetl plik

@ -627,7 +627,7 @@ class Object(StringIdModel):
ATProto = PROTOCOLS['atproto']
handle = ATProto(id=owner).handle
obj = bluesky.to_as1(self.bsky, repo_did=owner, repo_handle=handle,
uri=self.key.id(), pds=ATProto.target_for(self))
uri=self.key.id(), pds=ATProto.pds_for(self))
elif self.mf2:
obj = microformats2.json_to_object(self.mf2,
@ -868,9 +868,6 @@ class Object(StringIdModel):
# TODO: optimize! this is called serially in loops, eg in home.html
if set(actor.keys()) == {'id'} and self.source_protocol:
proto = PROTOCOLS[self.source_protocol]
# STATE: this load gets the DID doc, not the profile object
# should we start using at://[did] as actor/author? or special case
# atproto here?
actor_obj = proto.load(actor['id'], remote=False)
if actor_obj and actor_obj.as1:
actor = actor_obj.as1

Wyświetl plik

@ -716,7 +716,7 @@ class Protocol:
# fall through to deliver to followers
# fetch actor if necessary so we have name, profile photo, etc
# fetch actor if necessary
if actor and actor.keys() == set(['id']):
logger.info('Fetching actor so we have name, profile photo, etc')
actor_obj = from_cls.load(actor['id'])
@ -1112,6 +1112,7 @@ class Protocol:
Raises:
requests.HTTPError: anything that :meth:`fetch` raises
"""
assert id
assert local or remote is not False
# logger.debug(f'Loading Object {id} local={local} remote={remote}')

Wyświetl plik

@ -35,7 +35,7 @@ Flask==3.0.2
Flask-Caching==2.1.0
flask-gae-static==1.0
flask-sock==0.7.0
google-api-core[grpc]==2.11.1
google-api-core[grpc]==2.17.1
google-auth==2.15.0
google-cloud-appengine-logging==1.4.3
google-cloud-audit-log==0.2.5
@ -43,7 +43,7 @@ google-cloud-core==2.3.2
google-cloud-datastore==2.16.1
google-cloud-dns==0.35.0
google-cloud-error-reporting==1.10.0
google-cloud-logging==3.9.0
google-cloud-logging==3.10.0
google-cloud-ndb==2.3.0
google-cloud-tasks==2.16.3
googleapis-common-protos==1.63.0
@ -98,7 +98,7 @@ testtools==2.7.1
tlslite-ng==0.7.6
tweepy==4.14.0
typing-extensions==4.9.0
typing-validation==1.2.10.post4
typing-validation==1.2.11.post1
ujson==5.9.0
update-checker==0.18.0
urllib3==2.2.1

Wyświetl plik

@ -52,6 +52,14 @@ DID_DOC = {
}
BLOB_CID = CID.decode('bafkreicqpqncshdd27sgztqgzocd3zhhqnnsv6slvzhs5uz6f57cq6lmtq')
NOTE_AS = {
'objectType': 'note',
'id': 'fake:post',
'content': 'My original post',
'actor': 'fake:user',
'published': '2007-07-07T03:04:05.000Z',
}
class ATProtoTest(TestCase):
@ -61,15 +69,16 @@ class ATProtoTest(TestCase):
common.RUN_TASKS_INLINE = False
def make_user_and_repo(self):
user = self.make_user(id='fake:user', cls=Fake,
copies=[Target(uri='did:plc:user', protocol='atproto')])
self.user = self.make_user(id='fake:user', cls=Fake,
copies=[Target(uri='did:plc:user',
protocol='atproto')])
did_doc = copy.deepcopy(DID_DOC)
did_doc['service'][0]['serviceEndpoint'] = 'https://atproto.brid.gy/'
self.store_object(id='did:plc:user', raw=did_doc)
Repo.create(self.storage, 'did:plc:user', signing_key=ATPROTO_KEY)
return user
return self.user
@patch('requests.get', return_value=requests_response(DID_DOC))
def test_put_validates_id(self, mock_get):
@ -147,6 +156,14 @@ class ATProtoTest(TestCase):
got = ATProto.pds_for(Object(id='at://did:plc:user/co.ll/123'))
self.assertEqual('https://some.pds', got)
def test_pds_for_bsky_record_stored_did(self):
# check that we don't use Object.as1, which would cause an infinite loop
self.assertIsNone(ATProto.pds_for(Object(id='at://did:bob/coll/post', bsky={
'$type': 'app.bsky.feed.post',
'uri': 'at://did:bob/coll/post',
'cid': 'my sidd',
})))
@patch('requests.get', return_value=requests_response(DID_DOC))
def test_pds_for_fetch_did(self, mock_get):
got = ATProto.pds_for(Object(id='at://did:plc:user/co.ll/123'))
@ -156,10 +173,7 @@ class ATProtoTest(TestCase):
self.store_object(id='did:plc:user', raw=DID_DOC)
self.make_user('fake:user', cls=Fake,
copies=[Target(uri='did:plc:user', protocol='atproto')])
got = ATProto.pds_for(Object(id='fake:post', our_as1={
**POST_AS,
'actor': 'fake:user',
}))
got = ATProto.pds_for(Object(id='fake:post', our_as1=NOTE_AS))
self.assertEqual('https://some.pds', got)
def test_pds_for_user_no_stored_did(self):
@ -277,39 +291,81 @@ class ATProtoTest(TestCase):
'https://api.bsky-sandbox.dev/xrpc/com.atproto.repo.getRecord?repo=did%3Aplc%3Aabc&collection=app.bsky.feed.post&rkey=123',
json=None, data=None, headers=ANY)
def test_fetch_bsky_app_url_fails(self):
for uri in ('https://bsky.app/profile/han.dull',
'https://bsky.app/profile/han.dull/post/789'):
with self.assertRaises(AssertionError):
ATProto.fetch(Object(id=uri))
@patch('dns.resolver.resolve', side_effect=dns.resolver.NXDOMAIN())
@patch('requests.get', return_value=requests_response(status=404))
def test_fetch_resolve_handle_fails(self, mock_get, _):
obj = Object(id='at://bad.com/app.bsky.feed.post/789')
self.assertFalse(ATProto.fetch(obj))
def test_load_did_doc(self):
obj = self.store_object(id='did:plc:user', raw=DID_DOC)
self.assert_entities_equal(obj, ATProto.load('did:plc:user', did_doc=True))
def test_load_did_doc_false_loads_profile(self):
did_doc = self.store_object(id='did:plc:user', raw=DID_DOC)
profile = self.store_object(id='at://did:plc:user/app.bsky.actor.profile/self',
bsky=ACTOR_PROFILE_BSKY)
self.assert_entities_equal(profile, ATProto.load('did:plc:user'))
@patch('dns.resolver.resolve', side_effect=dns.resolver.NXDOMAIN())
@patch('requests.get', side_effect=[
# resolving handle, HTTPS method
requests_response('did:plc:abc', content_type='text/plain'),
requests_response('did:plc:user', content_type='text/plain'),
# AppView getRecord
requests_response({
'cid': 'bafy...',
'value': {'foo': 'bar'},
'value': {'$type': 'app.bsky.actor.profile', 'bar': 'baz'},
}),
# fetching DID doc
requests_response(DID_DOC),
])
def test_fetch_bsky_app_url(self, mock_get, _):
obj = Object(id='https://bsky.app/profile/han.dull/post/789')
self.assertTrue(ATProto.fetch(obj))
self.assertEqual('at://did:plc:abc/app.bsky.feed.post/789', obj.key.id())
def test_load_bsky_app_post_url(self, mock_get, _):
obj = ATProto.load('https://bsky.app/profile/han.dull/post/789')
self.assertEqual('at://did:plc:user/app.bsky.feed.post/789', obj.key.id())
self.assertEqual({
'foo': 'bar',
'$type': 'app.bsky.actor.profile',
'bar': 'baz',
'cid': 'bafy...',
}, obj.bsky)
mock_get.assert_any_call(
'https://api.bsky-sandbox.dev/xrpc/com.atproto.repo.getRecord?repo=did%3Aplc%3Auser&collection=app.bsky.feed.post&rkey=789',
json=None, data=None, headers={
'Content-Type': 'application/json',
'User-Agent': common.USER_AGENT,
})
self.assert_req(mock_get, 'https://plc.local/did:plc:user')
@patch('requests.get', return_value=requests_response({
'cid': 'bafy...',
'value': {'$type': 'app.bsky.actor.profile', 'bar': 'baz'},
}))
def test_load_bsky_profile_url(self, mock_get):
self.store_object(id='did:plc:user', raw=DID_DOC)
self.make_user('did:plc:user', cls=ATProto)
obj = ATProto.load('https://bsky.app/profile/han.dull')
self.assertEqual('at://did:plc:user/app.bsky.actor.profile/self', obj.key.id())
self.assertEqual({
'$type': 'app.bsky.actor.profile',
'bar': 'baz',
'cid': 'bafy...',
}, obj.bsky)
mock_get.assert_called_with(
'https://api.bsky-sandbox.dev/xrpc/com.atproto.repo.getRecord?repo=did%3Aplc%3Aabc&collection=app.bsky.feed.post&rkey=789',
'https://api.bsky-sandbox.dev/xrpc/com.atproto.repo.getRecord?repo=did%3Aplc%3Auser&collection=app.bsky.actor.profile&rkey=self',
json=None, data=None, headers={
'Content-Type': 'application/json',
'User-Agent': common.USER_AGENT,
},
)
@patch('dns.resolver.resolve', side_effect=dns.resolver.NXDOMAIN())
@patch('requests.get', return_value=requests_response(status=404))
def test_fetch_resolve_handle_fails(self, mock_get, _):
obj = Object(id='https://bsky.app/profile/bad.com/post/789')
self.assertFalse(ATProto.fetch(obj))
def test_convert_bsky_pass_through(self):
self.assertEqual({
'foo': 'bar',
@ -587,7 +643,7 @@ class ATProtoTest(TestCase):
did = user.get_copy(ATProto)
assert did
self.assertEqual([Target(uri=did, protocol='atproto')], user.copies)
did_obj = ATProto.load(did)
did_obj = ATProto.load(did, did_doc=True)
self.assertEqual('https://atproto.brid.gy/',
did_obj.raw['service'][0]['serviceEndpoint'])
@ -643,10 +699,7 @@ class ATProtoTest(TestCase):
_, __):
Fake.fetchable = {'fake:user': ACTOR_AS}
obj = self.store_object(id='fake:post', source_protocol='fake', our_as1={
**POST_AS,
'actor': 'fake:user',
})
obj = self.store_object(id='fake:post', source_protocol='fake', our_as1=NOTE_AS)
self.assertTrue(ATProto.send(obj, 'https://atproto.brid.gy/'))
# check profile, record
@ -682,10 +735,8 @@ class ATProtoTest(TestCase):
@patch.object(tasks_client, 'create_task', return_value=Task(name='my task'))
def test_send_note_existing_repo(self, mock_create_task):
user = self.make_user_and_repo()
obj = self.store_object(id='fake:post', source_protocol='fake', our_as1={
**POST_AS,
'actor': 'fake:user',
})
obj = self.store_object(id='fake:post', source_protocol='fake',
our_as1=NOTE_AS)
self.assertTrue(ATProto.send(obj, 'https://atproto.brid.gy/'))
# check repo, record
@ -701,6 +752,52 @@ class ATProtoTest(TestCase):
mock_create_task.assert_called()
@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()
mock_create_task.reset_mock()
note = Object.get_by_id('fake:post')
note.our_as1['content'] = 'something new'
note.put()
update = self.store_object(id='fake:update', source_protocol='fake', our_as1={
'objectType': 'activity',
'verb': 'update',
'object': note.our_as1,
})
self.assertTrue(ATProto.send(update, 'https://atproto.brid.gy/'))
# check repo, record
did = self.user.key.get().get_copy(ATProto)
repo = self.storage.load_repo(did)
last_tid = arroba.util.int_to_tid(arroba.util._tid_ts_last)
record = repo.get_record('app.bsky.feed.post', last_tid)
self.assertEqual('something new', record['text'])
mock_create_task.assert_called()
@patch.object(tasks_client, 'create_task', return_value=Task(name='my task'))
def test_send_delete_note(self, mock_create_task):
self.test_send_note_existing_repo()
mock_create_task.reset_mock()
update = self.store_object(id='fake:delete', source_protocol='fake', our_as1={
'objectType': 'activity',
'verb': 'delete',
'actor': 'fake:user',
'object': 'fake:post',
})
self.assertTrue(ATProto.send(update, 'https://atproto.brid.gy/'))
# check repo, record
did = self.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.assertIsNone(repo.get_record('app.bsky.feed.post', last_tid))
mock_create_task.assert_called()
@patch.object(tasks_client, 'create_task', return_value=Task(name='my task'))
def test_send_like(self, mock_create_task):
user = self.make_user_and_repo()
@ -843,11 +940,8 @@ class ATProtoTest(TestCase):
self.store_object(id='did:plc:user', raw=DID_DOC) # uses https://some.pds
user = self.make_user(id='fake:user', cls=Fake,
copies=[Target(uri='did:plc:user', protocol='atproto')])
obj = self.store_object(id='fake:post', source_protocol='fake', our_as1={
'objectType': 'note',
'content': 'foo',
'actor': 'fake:user',
})
obj = self.store_object(id='fake:post', source_protocol='fake',
our_as1=NOTE_AS)
self.assertFalse(ATProto.send(obj, 'https://atproto.brid.gy/'))
self.assertEqual(0, AtpBlock.query().count())
self.assertEqual(0, AtpRepo.query().count())
@ -1144,7 +1238,7 @@ class ATProtoTest(TestCase):
post_obj = Object.get_by_id('at://did:web:alice.com/app.bsky.feed.post/123')
self.assertEqual(post, post_obj.bsky)
self.assert_task(mock_create_task, 'receive', '/queue/receive',
obj=post_obj.key.urlsafe(), authed_as='did:plc:a')
obj=post_obj.key.urlsafe(), authed_as='did:web:alice.com')
# TODO: https://github.com/snarfed/bridgy-fed/issues/728
# repost_obj = Object.get_by_id('at://did:plc:d/app.bsky.feed.post/456')

Wyświetl plik

@ -9,6 +9,7 @@ from oauth_dropins.webutil.testutil import requests_response
from activitypub import ActivityPub
import app
from atproto import ATProto
from granary.tests.test_bluesky import ACTOR_PROFILE_BSKY
import hub
from models import Target
from web import Web
@ -61,8 +62,8 @@ class IntegrationTests(TestCase):
Target(uri='at://did:plc:bob/app.bsky.feed.post/123', protocol='atproto'),
])
# ATProto listNotifications => receive
mock_get.side_effect = [
# ATProto listNotifications
requests_response({
'cursor': '...',
'notifications': [{
@ -90,6 +91,12 @@ class IntegrationTests(TestCase):
},
}],
}),
# ATProto getRecord of alice's profile
requests_response({
'uri': 'at://did:plc:alice/app.bsky.actor.profile/self',
'cid': 'alice sidd',
'value': test_atproto.ACTOR_PROFILE_BSKY,
}),
]
resp = self.post('/queue/atproto-poll-notifs', client=hub.app.test_client())
@ -136,7 +143,6 @@ class IntegrationTests(TestCase):
bob = self.make_user(id='bob.com', cls=Web,
copies=[Target(uri='did:plc:bob', protocol='atproto')])
# ATProto listNotifications => receive
mock_get.side_effect = [
# ATProto listNotifications
requests_response({
@ -157,6 +163,12 @@ class IntegrationTests(TestCase):
},
}],
}),
# ATProto getRecord of alice's profile
requests_response({
'uri': 'at://did:plc:alice/app.bsky.actor.profile/self',
'cid': 'alice sidd',
'value': test_atproto.ACTOR_PROFILE_BSKY,
}),
# webmention discovery
test_web.WEBMENTION_REL_LINK,
]

Wyświetl plik

@ -642,6 +642,23 @@ class ObjectTest(TestCase):
obj = Object(id='at://did:plc:foo/like/123', bsky=like_bsky)
self.assert_equals(like_as1, obj.as1)
def test_as1_from_bsky_image_blob(self):
self.store_object(id='did:web:alice.com', raw={
**DID_DOC,
'alsoKnownAs': ['at://alice.com'],
})
obj = Object(id='at://did:web:alice.com/app.bsky.actor.profile/self', bsky={
**ACTOR_PROFILE_BSKY,
'banner': None,
})
self.assert_equals({
**ACTOR_AS,
'image': [{
'url': 'https://some.pds/xrpc/com.atproto.sync.getBlob?did=did:web:alice.com&cid=bafkreim',
}],
}, obj.as1)
def test_as1_from_mf2_uses_url_as_id(self):
obj = Object(mf2={
'properties': {