kopia lustrzana https://github.com/snarfed/bridgy-fed
Protocol.targets: find and add originals for targets that are copies
both Object and User originalspull/642/head
rodzic
c1880569b8
commit
5214c77f6a
|
@ -9,8 +9,9 @@ Bridgy Fed connects your web site to
|
|||
`microformats2 <https://microformats.org/wiki/microformats2>`__. Your
|
||||
site gets its own fediverse profile, posts and avatar and header and
|
||||
all. Bridgy Fed translates likes, reposts, mentions, follows, and more
|
||||
back and forth. `See the user docs <https://fed.brid.gy/docs>`__ for
|
||||
more details.
|
||||
back and forth. `See the user docs <https://fed.brid.gy/docs>`__ and
|
||||
`developer docs <https://bridgy-fed.readthedocs.io/>`__ for more
|
||||
details.
|
||||
|
||||
https://fed.brid.gy/
|
||||
|
||||
|
@ -23,7 +24,9 @@ License: This project is placed in the public domain.
|
|||
Development
|
||||
-----------
|
||||
|
||||
Pull requests are welcome! Feel free to `ping me in
|
||||
Development reference docs are at
|
||||
`bridgy-fed.readthedocs.io <https://bridgy-fed.readthedocs.io/>`__. Pull
|
||||
requests are welcome! Feel free to `ping me in
|
||||
#indieweb-dev <https://indieweb.org/discuss>`__ with any questions.
|
||||
|
||||
First, fork and clone this repo. Then, install the `Google Cloud
|
||||
|
@ -80,6 +83,36 @@ added you as an owner - run:
|
|||
|
||||
gcloud -q beta app deploy --no-cache --project bridgy-federated *.yaml
|
||||
|
||||
How to add a new protocol
|
||||
-------------------------
|
||||
|
||||
1. Determine `how you’ll map the new protocol to other existing Bridgy
|
||||
Fed protocols <https://fed.brid.gy/docs#translate>`__, specifically
|
||||
identity, protocol inference, events, and operations. `Add those to
|
||||
the existing tables in the
|
||||
docs <https://github.com/snarfed/bridgy-fed/blob/main/templates/docs.html>`__
|
||||
in a PR. This is an important step before you start writing code.
|
||||
2. If the new protocol uses a new data format - which is likely - add
|
||||
that format to `granary <https://github.com/snarfed/granary>`__ in a
|
||||
new file with functions that convert to/from `ActivityStreams
|
||||
1 <https://activitystrea.ms/specs/json/1.0/>`__ and tests. See
|
||||
`nostr.py <https://github.com/snarfed/granary/blob/main/granary/nostr.py#L542>`__
|
||||
and
|
||||
`test_nostr.py <https://github.com/snarfed/granary/blob/main/granary/tests/test_nostr.py#>`__
|
||||
for examples.
|
||||
3. Implement the protocol in a new ``.py`` file as a subclass of both
|
||||
`Protocol <https://github.com/snarfed/bridgy-fed/blob/main/protocol.py>`__
|
||||
and
|
||||
`User <https://github.com/snarfed/bridgy-fed/blob/main/models.py>`__.
|
||||
Implement the ``send``, ``fetch``, ``serve``, and ``target_for``
|
||||
methods from ``Protocol`` and ``readable_id``, ``web_url``,
|
||||
``ap_address``, and ``ap_actor`` from ``User`` .
|
||||
4. TODO: add a new usage section to the docs for the new protocol.
|
||||
5. TODO: does the new protocol need any new UI or signup functionality?
|
||||
Unusual, but not impossible. Add that if necessary.
|
||||
6. Add the new protocol’s logo to ``static/``, use it in
|
||||
`templates/user.html <https://github.com/snarfed/bridgy-fed/blob/main/templates/user.html>`__.
|
||||
|
||||
Stats
|
||||
-----
|
||||
|
||||
|
@ -94,7 +127,7 @@ Bridgy <https://bridgy.readthedocs.io/#stats>`__. Here’s how.
|
|||
|
||||
::
|
||||
|
||||
gcloud datastore export --async gs://bridgy-federated.appspot.com/stats/ --kinds Follower,Response
|
||||
gcloud datastore export --async gs://bridgy-federated.appspot.com/stats/ --kinds Follower,Object
|
||||
|
||||
Note that ``--kinds`` is required. `From the export
|
||||
docs <https://cloud.google.com/datastore/docs/export-import-entities#limitations>`__:
|
||||
|
@ -109,7 +142,7 @@ Bridgy <https://bridgy.readthedocs.io/#stats>`__. Here’s how.
|
|||
|
||||
::
|
||||
|
||||
for kind in Follower Response; do
|
||||
for kind in Follower Object; do
|
||||
bq load --replace --nosync --source_format=DATASTORE_BACKUP datastore.$kind gs://bridgy-federated.appspot.com/stats/all_namespaces/kind_$kind/all_namespaces_kind_$kind.export_metadata
|
||||
done
|
||||
|
||||
|
|
45
protocol.py
45
protocol.py
|
@ -418,7 +418,7 @@ class Protocol:
|
|||
obj.changed = orig.changed
|
||||
|
||||
# if this is a post, ie not an activity, wrap it in a create or update
|
||||
obj = from_cls._handle_bare_object(obj)
|
||||
obj = from_cls.handle_bare_object(obj)
|
||||
|
||||
if obj.type not in SUPPORTED_TYPES:
|
||||
error(f'Sorry, {obj.type} activities are not supported yet.', status=501)
|
||||
|
@ -454,7 +454,7 @@ class Protocol:
|
|||
return 'OK' # noop
|
||||
|
||||
elif obj.type == 'stop-following':
|
||||
# TODO: unify with _handle_follow?
|
||||
# TODO: unify with handle_follow?
|
||||
# TODO: handle multiple followees
|
||||
if not actor_id or not inner_obj_id:
|
||||
error(f'Undo of Follow requires actor id and object id. Got: {actor_id} {inner_obj_id} {obj.as1}')
|
||||
|
@ -528,13 +528,13 @@ class Protocol:
|
|||
}
|
||||
|
||||
if obj.type == 'follow':
|
||||
from_cls._handle_follow(obj)
|
||||
from_cls.handle_follow(obj)
|
||||
|
||||
# deliver to targets
|
||||
return from_cls._deliver(obj)
|
||||
return from_cls.deliver(obj)
|
||||
|
||||
@classmethod
|
||||
def _handle_follow(from_cls, obj):
|
||||
def handle_follow(from_cls, obj):
|
||||
"""Handles an incoming follow activity.
|
||||
|
||||
Args:
|
||||
|
@ -626,7 +626,7 @@ class Protocol:
|
|||
accept.put()
|
||||
|
||||
@classmethod
|
||||
def _handle_bare_object(cls, obj):
|
||||
def handle_bare_object(cls, obj):
|
||||
"""If obj is a bare object, wraps it in a create or update activity.
|
||||
|
||||
Checks if we've seen it before.
|
||||
|
@ -691,7 +691,7 @@ class Protocol:
|
|||
error(f'{obj.key.id()} is unchanged, nothing to do', status=204)
|
||||
|
||||
@classmethod
|
||||
def _deliver(from_cls, obj):
|
||||
def deliver(from_cls, obj):
|
||||
"""Delivers an activity to its external recipients.
|
||||
|
||||
Args:
|
||||
|
@ -699,7 +699,7 @@ class Protocol:
|
|||
"""
|
||||
# find delivery targets
|
||||
# sort targets so order is deterministic for tests, debugging, etc
|
||||
targets = from_cls._targets(obj) # maps Target to Object or None
|
||||
targets = from_cls.targets(obj) # maps Target to Object or None
|
||||
|
||||
if not targets:
|
||||
obj.status = 'ignored'
|
||||
|
@ -759,24 +759,37 @@ class Protocol:
|
|||
return ret
|
||||
|
||||
@classmethod
|
||||
def _targets(cls, obj):
|
||||
def targets(cls, obj):
|
||||
"""Collects the targets to send an :class:`models.Object` to.
|
||||
|
||||
Targets are both objects - original posts, events, etc - and actors.
|
||||
|
||||
Args:
|
||||
obj: :class:`models.Object`
|
||||
obj (:class:`models.Object`)
|
||||
|
||||
Returns: dict: {
|
||||
:class:`Target`: original (in response to) :class:`Object`, if any,
|
||||
otherwise None
|
||||
}
|
||||
Returns:
|
||||
dict: {
|
||||
:class:`Target`: original (in response to) :class:`models.Object`,
|
||||
if any, otherwise None
|
||||
}
|
||||
"""
|
||||
logger.info('Finding recipients and their targets')
|
||||
|
||||
target_uris = set(as1.targets(obj.as1))
|
||||
logger.info(f'Raw targets: {target_uris}')
|
||||
|
||||
if target_uris:
|
||||
origs = {u.key.id() for u in User.get_for_copies(target_uris)} | \
|
||||
{o.key.id() for o in Object.query(Object.copies.uri.IN(target_uris))}
|
||||
if origs:
|
||||
target_uris |= origs
|
||||
logger.info(f'Added originals: {origs}')
|
||||
|
||||
|
||||
orig_obj = None
|
||||
targets = {}
|
||||
for id in sorted(as1.targets(obj.as1)):
|
||||
targets = {} # maps Target to Object or None
|
||||
|
||||
for id in sorted(target_uris):
|
||||
protocol = Protocol.for_id(id)
|
||||
if not protocol:
|
||||
logger.info(f"Can't determine protocol for {id}")
|
||||
|
|
|
@ -303,7 +303,49 @@ class ProtocolTest(TestCase):
|
|||
self.assertCountEqual([
|
||||
Target(protocol='fake', uri='fake:post:target'),
|
||||
Target(protocol='atproto', uri='http://localhost/'),
|
||||
], Protocol._targets(obj).keys())
|
||||
], Protocol.targets(obj).keys())
|
||||
|
||||
@patch('requests.get', return_value=requests_response({}))
|
||||
def test_targets_converts_copies_to_originals(self, mock_get):
|
||||
"""targets should convert User/Object.copies to their originals."""
|
||||
alice = self.make_user('fake:alice', cls=Fake,
|
||||
copies=[Target(uri='did:plc:alice', protocol='atproto')])
|
||||
bob = self.make_user(
|
||||
'fake:bob', cls=OtherFake,
|
||||
copies=[Target(uri='other:bob', protocol='other')])
|
||||
obj = self.store_object(
|
||||
id='fake:post', our_as1={'foo': 9},
|
||||
copies=[Target(uri='at://did:plc:eve/post/789', protocol='fake')])
|
||||
|
||||
Fake.fetchable = {
|
||||
'fake:alice': {'foo': 1},
|
||||
'fake:bob': {'foo': 2},
|
||||
}
|
||||
OtherFake.fetchable = {
|
||||
'other:bob': {'foo': 3},
|
||||
}
|
||||
|
||||
obj = Object(our_as1={
|
||||
'id': 'other:reply',
|
||||
'objectType': 'note',
|
||||
'inReplyTo': [
|
||||
'at://did:web:unknown/post/123',
|
||||
'at://did:plc:eve/post/789',
|
||||
],
|
||||
'tags': [{
|
||||
'objectType': 'mention',
|
||||
'url': 'did:plc:alice',
|
||||
}, {
|
||||
'objectType': 'mention',
|
||||
'url': 'other:bob',
|
||||
}],
|
||||
})
|
||||
self.assertCountEqual([
|
||||
Target(uri='fake:post:target', protocol='fake'),
|
||||
Target(uri='fake:alice:target', protocol='fake'),
|
||||
Target(uri='fake:bob:target', protocol='fake'),
|
||||
Target(uri='other:bob:target', protocol='otherfake'),
|
||||
], Protocol.targets(obj).keys())
|
||||
|
||||
|
||||
class ProtocolReceiveTest(TestCase):
|
||||
|
@ -1161,9 +1203,11 @@ class ProtocolReceiveTest(TestCase):
|
|||
|
||||
self.assertEqual('OK', OtherFake.receive_as1(follow_as1))
|
||||
|
||||
self.assertEqual(2, len(Fake.sent))
|
||||
self.assertEqual('accept', Fake.sent[0][0].type)
|
||||
self.assertEqual('follow', Fake.sent[1][0].type)
|
||||
self.assertEqual(1, len(OtherFake.sent))
|
||||
self.assertEqual('accept', OtherFake.sent[0][0].type)
|
||||
|
||||
self.assertEqual(1, len(Fake.sent))
|
||||
self.assertEqual('follow', Fake.sent[0][0].type)
|
||||
|
||||
followers = Follower.query().fetch()
|
||||
self.assertEqual(1, len(followers))
|
||||
|
|
|
@ -140,6 +140,10 @@ class OtherFake(Fake):
|
|||
"""
|
||||
ABBREV = 'other'
|
||||
|
||||
fetchable = {}
|
||||
sent = []
|
||||
fetched = []
|
||||
|
||||
@classmethod
|
||||
def owns_id(cls, id):
|
||||
return id.startswith('other:')
|
||||
|
|
Ładowanie…
Reference in New Issue