kopia lustrzana https://github.com/snarfed/bridgy-fed
create AP users on inbox delivery for an indirect or nonexistent Web user
for #512circle-datastore-transactions
rodzic
086c6d032c
commit
28eabd07a3
|
@ -48,7 +48,10 @@ def default_signature_user():
|
|||
|
||||
|
||||
class ActivityPub(User, Protocol):
|
||||
"""ActivityPub protocol class."""
|
||||
"""ActivityPub protocol class.
|
||||
|
||||
Key id is AP/AS2 actor id URL. (*Not* fediverse/WebFinger @-@ handle!)
|
||||
"""
|
||||
LABEL = 'activitypub'
|
||||
|
||||
@classmethod
|
||||
|
@ -566,14 +569,20 @@ def inbox(protocol=None, domain=None):
|
|||
error(f"Couldn't parse body as non-empty JSON mapping: {body}", exc_info=True)
|
||||
|
||||
type = activity.get('type')
|
||||
actor_id = as1.get_object(activity, 'actor').get('id')
|
||||
actor = as1.get_object(activity, 'actor')
|
||||
actor_id = actor.get('id')
|
||||
logger.info(f'Got {type} from {actor_id}: {json_dumps(activity, indent=2)}')
|
||||
|
||||
# load user
|
||||
# TODO(#512) parameterize on protocol, move to Protocol
|
||||
if protocol and domain:
|
||||
g.user = PROTOCOLS[protocol].get_by_id(domain)
|
||||
if not g.user:
|
||||
error(f'{protocol} user {domain} not found', status=404)
|
||||
g.user = PROTOCOLS[protocol].get_by_id(domain) # receiving user
|
||||
if (not g.user or not g.user.direct) and actor_id:
|
||||
# this is a deliberate interaction with an indirect receiving user;
|
||||
# create a local AP User for the sending user
|
||||
actor_obj = ActivityPub.load(actor_id)
|
||||
ActivityPub.get_or_create(actor_id, direct=True,
|
||||
actor_as2=as2.from_as1(actor_obj.as1))
|
||||
|
||||
ActivityPub.verify_signature(activity)
|
||||
|
||||
|
|
10
models.py
10
models.py
|
@ -79,9 +79,9 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
|
|||
property: p256_key, PEM encoded
|
||||
https://atproto.com/guides/overview#account-portability
|
||||
"""
|
||||
mod = ndb.StringProperty(required=True)
|
||||
public_exponent = ndb.StringProperty(required=True)
|
||||
private_exponent = ndb.StringProperty(required=True)
|
||||
mod = ndb.StringProperty()
|
||||
public_exponent = ndb.StringProperty()
|
||||
private_exponent = ndb.StringProperty()
|
||||
p256_key = ndb.StringProperty()
|
||||
has_redirects = ndb.BooleanProperty()
|
||||
redirects_error = ndb.TextProperty()
|
||||
|
@ -129,7 +129,8 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
|
|||
if user:
|
||||
# override direct if it's set
|
||||
direct = kwargs.get('direct')
|
||||
if direct is not None:
|
||||
if direct is not None and direct != user.direct:
|
||||
logger.info(f'Setting {user.key} direct={direct}')
|
||||
user.direct = direct
|
||||
user.put()
|
||||
return user
|
||||
|
@ -153,6 +154,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
|
|||
kwargs['p256_key'] = key.export_key(format='PEM')
|
||||
|
||||
user = cls(id=id, **kwargs)
|
||||
logger.info(f'Created new {user}')
|
||||
user.put()
|
||||
return user
|
||||
|
||||
|
|
|
@ -109,15 +109,21 @@ LIKE = {
|
|||
}
|
||||
LIKE_WRAPPED = copy.deepcopy(LIKE)
|
||||
LIKE_WRAPPED['object'] = 'http://localhost/r/https://user.com/post'
|
||||
LIKE_WITH_ACTOR = copy.deepcopy(LIKE)
|
||||
# TODO: use ACTOR instead
|
||||
LIKE_WITH_ACTOR['actor'] = {
|
||||
LIKE_ACTOR = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'id': 'https://user.com/actor',
|
||||
'type': 'Person',
|
||||
'name': 'Ms. Actor',
|
||||
'preferredUsername': 'msactor',
|
||||
'image': {'type': 'Image', 'url': 'https://user.com/pic.jpg'},
|
||||
'icon': {'type': 'Image', 'url': 'https://user.com/pic.jpg'},
|
||||
'image': [
|
||||
{'type': 'Image', 'url': 'https://user.com/thumb.jpg'},
|
||||
{'type': 'Image', 'url': 'https://user.com/pic.jpg'},
|
||||
],
|
||||
}
|
||||
LIKE_WITH_ACTOR = {
|
||||
**LIKE,
|
||||
'actor': LIKE_ACTOR,
|
||||
}
|
||||
|
||||
# repost, should be delivered to followers if object is a fediverse post,
|
||||
|
@ -230,8 +236,7 @@ class ActivityPubTest(TestCase):
|
|||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = self.make_user('user.com',
|
||||
has_hcard=True, actor_as2=ACTOR)
|
||||
self.user = self.make_user('user.com', has_hcard=True, actor_as2=ACTOR)
|
||||
with self.request_context:
|
||||
self.key_id_obj = Object(id='http://my/key/id', as2={
|
||||
**ACTOR,
|
||||
|
@ -323,9 +328,23 @@ class ActivityPubTest(TestCase):
|
|||
got = self.client.get('/nope.com')
|
||||
self.assertEqual(404, got.status_code)
|
||||
|
||||
def test_individual_inbox_no_user(self, *mocks):
|
||||
got = self.post('/nope.com/inbox', json=REPLY)
|
||||
self.assertEqual(404, got.status_code)
|
||||
def test_individual_inbox_no_user(self, mock_head, mock_get, mock_post):
|
||||
self.user.key.delete()
|
||||
|
||||
mock_get.side_effect = [self.as2_resp(LIKE_ACTOR)]
|
||||
|
||||
reply = {
|
||||
**REPLY,
|
||||
'actor': LIKE_ACTOR,
|
||||
}
|
||||
got = self._test_inbox_reply(reply, {
|
||||
'as2': reply,
|
||||
'type': 'post',
|
||||
'labels': ['activity', 'notification'],
|
||||
}, mock_head, mock_get, mock_post)
|
||||
|
||||
self.assert_user(ActivityPub, 'https://user.com/actor',
|
||||
actor_as2=LIKE_ACTOR, direct=True)
|
||||
|
||||
def test_inbox_activity_without_id(self, *_):
|
||||
note = copy.deepcopy(NOTE)
|
||||
|
@ -362,7 +381,9 @@ class ActivityPubTest(TestCase):
|
|||
|
||||
def _test_inbox_reply(self, reply, expected_props, mock_head, mock_get, mock_post):
|
||||
mock_head.return_value = requests_response(url='https://user.com/post')
|
||||
mock_get.return_value = WEBMENTION_DISCOVERY
|
||||
mock_get.side_effect = (
|
||||
(list(mock_get.side_effect) if mock_get.side_effect else [])
|
||||
+ [WEBMENTION_DISCOVERY])
|
||||
mock_post.return_value = requests_response()
|
||||
|
||||
got = self.post('/user.com/inbox', json=reply)
|
||||
|
@ -640,6 +661,17 @@ class ActivityPubTest(TestCase):
|
|||
labels=['notification', 'activity'],
|
||||
object_ids=[LIKE['object']])
|
||||
|
||||
def test_inbox_like_indirect_user_creates_User(self, mock_get, *_):
|
||||
self.user.direct = False
|
||||
self.user.put()
|
||||
|
||||
mock_get.return_value = self.as2_resp(LIKE_ACTOR)
|
||||
|
||||
self.test_inbox_like()
|
||||
self.assert_user(ActivityPub, 'https://user.com/actor',
|
||||
actor_as2=LIKE_ACTOR, direct=True)
|
||||
|
||||
|
||||
def test_inbox_follow_accept_with_id(self, *mocks):
|
||||
self._test_inbox_follow_accept(FOLLOW_WRAPPED, ACCEPT, *mocks)
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@ with ndb_client.context():
|
|||
models.reset_protocol_properties()
|
||||
|
||||
import app
|
||||
import activitypub
|
||||
from activitypub import ActivityPub, CONNEG_HEADERS_AS2_HTML
|
||||
import common
|
||||
from web import Web
|
||||
from flask_app import app, cache, init_globals
|
||||
|
@ -184,7 +184,7 @@ class TestCase(unittest.TestCase, testutil.Asserts):
|
|||
return f'com.example.record/{tid}'
|
||||
|
||||
def get_as2(self, *args, **kwargs):
|
||||
kwargs.setdefault('headers', {})['Accept'] = activitypub.CONNEG_HEADERS_AS2_HTML
|
||||
kwargs.setdefault('headers', {})['Accept'] = CONNEG_HEADERS_AS2_HTML
|
||||
return self.client.get(*args, **kwargs)
|
||||
|
||||
def req(self, url, **kwargs):
|
||||
|
@ -202,7 +202,7 @@ class TestCase(unittest.TestCase, testutil.Asserts):
|
|||
'Host': util.domain_from_link(url, minimize=False),
|
||||
'Content-Type': 'application/activity+json',
|
||||
'Digest': ANY,
|
||||
**activitypub.CONNEG_HEADERS_AS2_HTML,
|
||||
**CONNEG_HEADERS_AS2_HTML,
|
||||
**kwargs.pop('headers', {}),
|
||||
}
|
||||
return self.req(url, data=None, auth=ANY, headers=headers,
|
||||
|
@ -250,6 +250,23 @@ class TestCase(unittest.TestCase, testutil.Asserts):
|
|||
ignore=['as1', 'created', 'expire',
|
||||
'object_ids', 'type', 'updated'])
|
||||
|
||||
def assert_user(self, cls, id, **props):
|
||||
got = cls.get_by_id(id)
|
||||
assert got, id
|
||||
|
||||
self.assert_entities_equal(
|
||||
cls(id=id, **props), got,
|
||||
ignore=['created', 'mod', 'p256_key', 'private_exponent',
|
||||
'public_exponent', 'updated'])
|
||||
|
||||
if cls != ActivityPub:
|
||||
assert got.mod
|
||||
assert got.private_exponent
|
||||
assert got.public_exponent
|
||||
|
||||
# if cls != ATProto:
|
||||
# assert got.p256_key
|
||||
|
||||
def assert_equals(self, expected, actual, msg=None, ignore=(), **kwargs):
|
||||
return super().assert_equals(
|
||||
expected, actual, msg=msg, ignore=tuple(ignore) + ('@context',), **kwargs)
|
||||
|
|
Ładowanie…
Reference in New Issue