kopia lustrzana https://github.com/snarfed/bridgy-fed
Porównaj commity
10 Commity
6325f0e94c
...
ca390a6ac7
Autor | SHA1 | Data |
---|---|---|
dependabot[bot] | ca390a6ac7 | |
dependabot[bot] | e37c2b2b9a | |
Ryan Barrett | 048cd17ba5 | |
Ryan Barrett | e86763f654 | |
Ryan Barrett | 878bbde328 | |
Ryan Barrett | 9b2bb9ef37 | |
Ryan Barrett | 5eac0e06d0 | |
Ryan Barrett | 69e4b039e9 | |
Ryan Barrett | bad7052663 | |
Ryan Barrett | d667c8ece4 |
103
atproto.py
103
atproto.py
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}')
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
|
|
|
@ -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': {
|
||||
|
|
Ładowanie…
Reference in New Issue