kopia lustrzana https://github.com/snarfed/bridgy-fed
rodzic
face71c9eb
commit
1591dfb641
|
@ -188,7 +188,7 @@ class ActivityPub(User, Protocol):
|
|||
return actor.get('publicInbox') or actor.get('inbox')
|
||||
|
||||
@classmethod
|
||||
def send(to_cls, obj, url, orig_obj=None):
|
||||
def send(to_cls, obj, url, from_user=None, orig_obj=None):
|
||||
"""Delivers an activity to an inbox URL.
|
||||
|
||||
If ``obj.recipient_obj`` is set, it's interpreted as the receiving actor
|
||||
|
@ -198,11 +198,10 @@ class ActivityPub(User, Protocol):
|
|||
logger.info(f'Skipping sending to blocklisted {url}')
|
||||
return False
|
||||
|
||||
activity = to_cls.convert(obj, orig_obj=to_cls.convert(orig_obj))
|
||||
if not activity.get('actor'):
|
||||
logger.warning('Outgoing AP activity has no actor!')
|
||||
activity = to_cls.convert(obj, from_user=from_user,
|
||||
orig_obj=to_cls.convert(orig_obj))
|
||||
|
||||
return signed_post(url, data=activity).ok
|
||||
return signed_post(url, data=activity, from_user=from_user).ok
|
||||
|
||||
@classmethod
|
||||
def fetch(cls, obj, **kwargs):
|
||||
|
@ -315,7 +314,7 @@ class ActivityPub(User, Protocol):
|
|||
return False
|
||||
|
||||
@classmethod
|
||||
def convert(cls, obj, orig_obj=None):
|
||||
def convert(cls, obj, orig_obj=None, from_user=None):
|
||||
"""Convert a :class:`models.Object` to AS2.
|
||||
|
||||
Args:
|
||||
|
@ -323,6 +322,7 @@ class ActivityPub(User, Protocol):
|
|||
orig_obj (dict): AS2 object, optional. The target of activity's
|
||||
``inReplyTo`` or ``Like``/``Announce``/etc object, if any. Passed
|
||||
through to :func:`postprocess_as2`.
|
||||
from_user (models.User): user (actor) this activity/object is from
|
||||
|
||||
Returns:
|
||||
dict: AS2 JSON
|
||||
|
@ -351,11 +351,12 @@ class ActivityPub(User, Protocol):
|
|||
if obj.source_protocol in ('ap', 'activitypub'):
|
||||
return converted
|
||||
|
||||
if converted.get('type') == 'Person':
|
||||
return postprocess_as2_actor(converted)
|
||||
if as1.object_type(obj.as1) in as1.ACTOR_TYPES:
|
||||
return postprocess_as2_actor(converted, user=from_user)
|
||||
|
||||
if as1.get_object(converted).get('type') == 'Person':
|
||||
converted['object'] = postprocess_as2_actor(converted['object'])
|
||||
if as1.object_type(as1.get_object(obj.as1)) in as1.ACTOR_TYPES:
|
||||
converted['object'] = postprocess_as2_actor(converted['object'],
|
||||
user=from_user)
|
||||
|
||||
return postprocess_as2(converted, orig_obj=orig_obj)
|
||||
|
||||
|
@ -456,16 +457,16 @@ class ActivityPub(User, Protocol):
|
|||
return keyId
|
||||
|
||||
|
||||
def signed_get(url, **kwargs):
|
||||
return signed_request(util.requests_get, url, **kwargs)
|
||||
def signed_get(url, from_user=None, **kwargs):
|
||||
return signed_request(util.requests_get, url, from_user=from_user, **kwargs)
|
||||
|
||||
|
||||
def signed_post(url, **kwargs):
|
||||
assert g.user
|
||||
return signed_request(util.requests_post, url, **kwargs)
|
||||
def signed_post(url, from_user, **kwargs):
|
||||
assert from_user
|
||||
return signed_request(util.requests_post, url, from_user=from_user, **kwargs)
|
||||
|
||||
|
||||
def signed_request(fn, url, data=None, headers=None, **kwargs):
|
||||
def signed_request(fn, url, data=None, headers=None, from_user=None, **kwargs):
|
||||
"""Wraps ``requests.*`` and adds HTTP Signature.
|
||||
|
||||
If the current session has a user (ie in ``g.user``), signs with that user's
|
||||
|
@ -475,6 +476,7 @@ def signed_request(fn, url, data=None, headers=None, **kwargs):
|
|||
fn (callable): :func:`util.requests_get` or :func:`util.requests_post`
|
||||
url (str):
|
||||
data (dict): optional AS2 object
|
||||
from_user (models.User): user to sign request as; optional
|
||||
kwargs: passed through to requests
|
||||
|
||||
Returns:
|
||||
|
@ -484,10 +486,9 @@ def signed_request(fn, url, data=None, headers=None, **kwargs):
|
|||
headers = {}
|
||||
|
||||
# prepare HTTP Signature and headers
|
||||
user = g.user
|
||||
if not user or isinstance(user, ActivityPub):
|
||||
if not from_user or isinstance(from_user, ActivityPub):
|
||||
# ActivityPub users are remote, so we don't have their keys
|
||||
user = default_signature_user()
|
||||
from_user = default_signature_user()
|
||||
|
||||
if data:
|
||||
logger.info(f'Sending AS2 object: {json_dumps(data, indent=2)}')
|
||||
|
@ -506,14 +507,14 @@ def signed_request(fn, url, data=None, headers=None, **kwargs):
|
|||
'Digest': f'SHA-256={b64encode(sha256(data or b"").digest()).decode()}',
|
||||
}
|
||||
|
||||
logger.info(f"Signing with {user.key}'s key")
|
||||
logger.info(f"Signing with {from_user.key}'s key")
|
||||
# (request-target) is a special HTTP Signatures header that some fediverse
|
||||
# implementations require, eg Peertube.
|
||||
# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.3
|
||||
# https://www.w3.org/wiki/SocialCG/ActivityPub/Authentication_Authorization#Signing_requests_using_HTTP_Signatures
|
||||
# https://docs.joinmastodon.org/spec/security/#http
|
||||
key_id = f'{user.ap_actor()}#key'
|
||||
auth = HTTPSignatureAuth(secret=user.private_pem(), key_id=key_id,
|
||||
key_id = f'{from_user.ap_actor()}#key'
|
||||
auth = HTTPSignatureAuth(secret=from_user.private_pem(), key_id=key_id,
|
||||
algorithm='rsa-sha256', sign_header='signature',
|
||||
headers=HTTP_SIG_HEADERS)
|
||||
|
||||
|
@ -541,9 +542,6 @@ def signed_request(fn, url, data=None, headers=None, **kwargs):
|
|||
def postprocess_as2(activity, orig_obj=None, wrap=True):
|
||||
"""Prepare an AS2 object to be served or sent via ActivityPub.
|
||||
|
||||
``g.user`` is required (in ``postprocess_as2_actor``). It's populated it
|
||||
into the ``actor.id`` and ``publicKey`` fields.
|
||||
|
||||
Args:
|
||||
activity (dict): AS2 object or activity
|
||||
orig_obj (dict): AS2 object, optional. The target of activity's
|
||||
|
@ -685,7 +683,7 @@ def postprocess_as2(activity, orig_obj=None, wrap=True):
|
|||
return util.trim_nulls(activity)
|
||||
|
||||
|
||||
def postprocess_as2_actor(actor, wrap=True):
|
||||
def postprocess_as2_actor(actor, user=None, wrap=True):
|
||||
"""Prepare an AS2 actor object to be served or sent via ActivityPub.
|
||||
|
||||
Modifies actor in place.
|
||||
|
@ -699,12 +697,10 @@ def postprocess_as2_actor(actor, wrap=True):
|
|||
"""
|
||||
if not actor:
|
||||
return actor
|
||||
elif isinstance(actor, str):
|
||||
if g.user and g.user.is_web_url(actor):
|
||||
return g.user.ap_actor()
|
||||
return redirect_wrap(actor)
|
||||
|
||||
url = g.user.web_url()
|
||||
assert isinstance(actor, dict)
|
||||
|
||||
url = user.web_url()
|
||||
urls = util.get_list(actor, 'url')
|
||||
if not urls and url:
|
||||
urls = [url]
|
||||
|
@ -712,14 +708,14 @@ def postprocess_as2_actor(actor, wrap=True):
|
|||
urls[0] = redirect_wrap(urls[0])
|
||||
|
||||
id = actor.get('id')
|
||||
if not id or g.user.is_web_url(id):
|
||||
actor['id'] = g.user.ap_actor()
|
||||
if not id or user.is_web_url(id):
|
||||
actor['id'] = user.ap_actor()
|
||||
|
||||
actor['url'] = urls[0] if len(urls) == 1 else urls
|
||||
# required by ActivityPub
|
||||
# https://www.w3.org/TR/activitypub/#actor-objects
|
||||
actor.setdefault('inbox', g.user.ap_actor('inbox'))
|
||||
actor.setdefault('outbox', g.user.ap_actor('outbox'))
|
||||
actor.setdefault('inbox', user.ap_actor('inbox'))
|
||||
actor.setdefault('outbox', user.ap_actor('outbox'))
|
||||
|
||||
# This has to be the id (domain for Web) for Mastodon etc interop! It
|
||||
# seems like it should be the custom username from the acct: u-url in
|
||||
|
@ -729,7 +725,7 @@ def postprocess_as2_actor(actor, wrap=True):
|
|||
# https://docs.joinmastodon.org/spec/webfinger/#mastodons-requirements-for-webfinger
|
||||
# https://github.com/snarfed/bridgy-fed/issues/302#issuecomment-1324305460
|
||||
# https://github.com/snarfed/bridgy-fed/issues/77
|
||||
handle = g.user.handle_as(ActivityPub)
|
||||
handle = user.handle_as(ActivityPub)
|
||||
if handle:
|
||||
actor['preferredUsername'] = handle.strip('@').split('@')[0]
|
||||
|
||||
|
@ -749,12 +745,12 @@ def postprocess_as2_actor(actor, wrap=True):
|
|||
# underspecified, inferred from this issue and Mastodon's implementation:
|
||||
# https://github.com/w3c/activitypub/issues/203#issuecomment-297553229
|
||||
# https://github.com/tootsuite/mastodon/blob/bc2c263504e584e154384ecc2d804aeb1afb1ba3/app/services/activitypub/process_account_service.rb#L77
|
||||
actor_url = g.user.ap_actor()
|
||||
actor_url = user.ap_actor()
|
||||
actor.update({
|
||||
'publicKey': {
|
||||
'id': f'{actor_url}#key',
|
||||
'owner': actor_url,
|
||||
'publicKeyPem': g.user.public_pem().decode(),
|
||||
'publicKeyPem': user.public_pem().decode(),
|
||||
},
|
||||
'@context': (util.get_list(actor, '@context') +
|
||||
['https://w3id.org/security/v1']),
|
||||
|
@ -799,13 +795,14 @@ def actor(handle_or_id):
|
|||
user = cls.get_or_create(id)
|
||||
if not user.obj or not user.obj.as1:
|
||||
user.obj = cls.load(user.profile_id(), gateway=True)
|
||||
if user.obj:
|
||||
user.obj.put()
|
||||
|
||||
g.user = user
|
||||
actor = ActivityPub.convert(user.obj) or {
|
||||
actor = ActivityPub.convert(user.obj, from_user=user) or {
|
||||
'@context': [as2.CONTEXT],
|
||||
'type': 'Person',
|
||||
}
|
||||
actor = postprocess_as2_actor(actor)
|
||||
actor = postprocess_as2_actor(actor, user=user)
|
||||
actor.update({
|
||||
'id': user.ap_actor(),
|
||||
'inbox': user.ap_actor('inbox'),
|
||||
|
|
|
@ -248,7 +248,7 @@ class ATProto(User, Protocol):
|
|||
user.put()
|
||||
|
||||
@classmethod
|
||||
def send(to_cls, obj, url, orig_obj=None):
|
||||
def send(to_cls, obj, url, from_user=None, orig_obj=None):
|
||||
"""Creates a record if we own its repo.
|
||||
|
||||
Creates the repo first if it doesn't exist.
|
||||
|
@ -375,7 +375,7 @@ class ATProto(User, Protocol):
|
|||
return True
|
||||
|
||||
@classmethod
|
||||
def convert(cls, obj, fetch_blobs=False):
|
||||
def convert(cls, obj, fetch_blobs=False, from_user=None):
|
||||
"""Converts a :class:`models.Object` to ``app.bsky.*`` lexicon JSON.
|
||||
|
||||
Args:
|
||||
|
@ -383,6 +383,7 @@ class ATProto(User, Protocol):
|
|||
fetch_blobs (bool): whether to fetch images and other blobs, store
|
||||
them in :class:`arroba.datastore_storage.AtpRemoteBlob`\s if they
|
||||
don't already exist, and fill them into the returned object.
|
||||
from_user (models.User): user (actor) this activity/object is from
|
||||
|
||||
Returns:
|
||||
dict: JSON object
|
||||
|
|
16
convert.py
16
convert.py
|
@ -79,22 +79,6 @@ def convert(dest, _, src=None):
|
|||
if obj.deleted or type == 'delete':
|
||||
return '', 410
|
||||
|
||||
# load g.user for AP since postprocess_as2 currently needs it. ugh.
|
||||
if dest_cls == ActivityPub:
|
||||
actor_id = as1.get_owner(obj.as1)
|
||||
if not actor_id and src_cls == Web:
|
||||
actor_id = util.domain_from_link(id, minimize=False)
|
||||
if not actor_id:
|
||||
error(f"Couldn't determine actor id for {obj.as1}")
|
||||
|
||||
user_key = src_cls.key_for(actor_id)
|
||||
if not user_key:
|
||||
error(f"Couldn't determine {src_cls.LABEL} key for {actor_id}")
|
||||
|
||||
g.user = user_key.get()
|
||||
if not g.user:
|
||||
error(f'No {src_cls.LABEL} user found for {actor_id}')
|
||||
|
||||
# convert and serve
|
||||
return dest_cls.convert(obj), {'Content-Type': dest_cls.CONTENT_TYPE}
|
||||
|
||||
|
|
|
@ -131,7 +131,7 @@ class FollowCallback(indieauth.Callback):
|
|||
labels=['user'], source_protocol='ui', status='complete',
|
||||
as2=follow_as2)
|
||||
g.user = user
|
||||
ActivityPub.send(follow_obj, inbox)
|
||||
ActivityPub.send(follow_obj, inbox, from_user=user)
|
||||
|
||||
Follower.get_or_create(from_=user, to=followee_user, status='active',
|
||||
follow=follow_obj.key)
|
||||
|
@ -219,7 +219,7 @@ class UnfollowCallback(indieauth.Callback):
|
|||
obj = Object(id=unfollow_id, users=[user.key], labels=['user'],
|
||||
source_protocol='ui', status='complete', as2=unfollow_as2)
|
||||
g.user = user
|
||||
ActivityPub.send(obj, inbox)
|
||||
ActivityPub.send(obj, inbox, from_user=user)
|
||||
|
||||
follower.status = 'inactive'
|
||||
follower.put()
|
||||
|
|
65
protocol.py
65
protocol.py
|
@ -354,7 +354,7 @@ class Protocol:
|
|||
return cls.key_for(owner)
|
||||
|
||||
@classmethod
|
||||
def send(to_cls, obj, url, orig_obj=None):
|
||||
def send(to_cls, obj, url, from_user=None, orig_obj=None):
|
||||
"""Sends an outgoing activity.
|
||||
|
||||
To be implemented by subclasses.
|
||||
|
@ -362,6 +362,7 @@ class Protocol:
|
|||
Args:
|
||||
obj (models.Object): with activity to send
|
||||
url (str): destination URL to send to
|
||||
from_user (models.User): user (actor) this activity is from
|
||||
orig_obj (models.Object): the "original object" that this object
|
||||
refers to, eg replies to or reposts or likes
|
||||
|
||||
|
@ -401,7 +402,7 @@ class Protocol:
|
|||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def convert(cls, obj):
|
||||
def convert(cls, obj, from_user=None):
|
||||
"""Converts an :class:`Object` to this protocol's data format.
|
||||
|
||||
For example, an HTML string for :class:`Web`, or a dict with AS2 JSON
|
||||
|
@ -413,6 +414,7 @@ class Protocol:
|
|||
|
||||
Args:
|
||||
obj (models.Object):
|
||||
from_user (models.User): user (actor) this activity/object is from
|
||||
|
||||
Returns:
|
||||
converted object in the protocol's native format, often a dict
|
||||
|
@ -542,6 +544,7 @@ class Protocol:
|
|||
|
||||
Args:
|
||||
obj (models.Object)
|
||||
authed_as (str): authenticated actor id who sent this activity
|
||||
|
||||
Returns:
|
||||
(str, int) tuple: (response body, HTTP status code) Flask response
|
||||
|
@ -584,10 +587,21 @@ class Protocol:
|
|||
logger.info(msg)
|
||||
return msg, 204
|
||||
|
||||
# authorization check
|
||||
# load actor user, check authorization
|
||||
actor = as1.get_owner(obj.as1)
|
||||
if authed_as and actor != authed_as:
|
||||
logger.warning(f"actor {actor} isn't authed user {authed_as}")
|
||||
if not actor:
|
||||
error(r'Activity missing actor or author', status=400)
|
||||
|
||||
if authed_as:
|
||||
assert isinstance(authed_as, str)
|
||||
if actor != authed_as:
|
||||
logger.warning(f"actor {actor} isn't authed user {authed_as}")
|
||||
|
||||
from_user = from_cls.get_or_create(id=actor)
|
||||
if from_user.status == 'opt-out':
|
||||
error(r'Actor {actor} is opted out', status=204)
|
||||
if not g.user:
|
||||
g.user = from_user
|
||||
|
||||
# update copy ids to originals
|
||||
obj.resolve_ids()
|
||||
|
@ -602,31 +616,16 @@ class Protocol:
|
|||
|
||||
# if this is a post, ie not an activity, wrap it in a create or update
|
||||
obj = from_cls.handle_bare_object(obj)
|
||||
obj.add('users', from_user.key)
|
||||
|
||||
if obj.type not in SUPPORTED_TYPES:
|
||||
error(f'Sorry, {obj.type} activities are not supported yet.', status=501)
|
||||
|
||||
# add owner(s)
|
||||
owner = as1.get_owner(obj.as1)
|
||||
if not owner:
|
||||
error(r'Activity missing actor or author', status=400)
|
||||
|
||||
actor_key = from_cls.actor_key(obj)
|
||||
# actor_key returns None if the user is opted out
|
||||
if not actor_key:
|
||||
error(r'Actor {owner} is opted out', status=204)
|
||||
|
||||
obj.add('users', actor_key)
|
||||
if not g.user:
|
||||
g.user = from_cls.get_or_create(id=actor_key.id())
|
||||
|
||||
inner_obj_as1 = as1.get_object(obj.as1)
|
||||
if obj.as1.get('verb') in ('post', 'update', 'delete'):
|
||||
inner_actor = as1.get_owner(inner_obj_as1)
|
||||
if inner_actor:
|
||||
user_key = from_cls.key_for(inner_actor)
|
||||
if user_key:
|
||||
obj.add('users', user_key)
|
||||
if inner_owner := as1.get_owner(inner_obj_as1):
|
||||
if inner_owner_key := from_cls.key_for(inner_owner):
|
||||
obj.add('users', inner_owner_key)
|
||||
|
||||
obj.source_protocol = from_cls.LABEL
|
||||
obj.put()
|
||||
|
@ -721,7 +720,7 @@ class Protocol:
|
|||
from_cls.handle_follow(obj)
|
||||
|
||||
# deliver to targets
|
||||
return from_cls.deliver(obj)
|
||||
return from_cls.deliver(obj, from_user=from_user)
|
||||
|
||||
@classmethod
|
||||
def handle_follow(from_cls, obj):
|
||||
|
@ -817,7 +816,7 @@ class Protocol:
|
|||
# https://github.com/snarfed/bridgy-fed/issues/690
|
||||
orig_g_user = g.user
|
||||
g.user = to_user
|
||||
sent = from_cls.send(accept, from_target)
|
||||
sent = from_cls.send(accept, from_target, from_user=to_user)
|
||||
g.user = orig_g_user
|
||||
|
||||
if sent:
|
||||
|
@ -892,11 +891,12 @@ 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, from_user):
|
||||
"""Delivers an activity to its external recipients.
|
||||
|
||||
Args:
|
||||
obj (models.Object): activity to deliver
|
||||
from_user (models.User): user (actor) this activity is from
|
||||
"""
|
||||
# find delivery targets
|
||||
targets = from_cls.targets(obj) # maps Target to Object or None
|
||||
|
@ -918,9 +918,9 @@ class Protocol:
|
|||
logger.info(f'Delivering to: {obj.undelivered}')
|
||||
|
||||
# enqueue send task for each targets
|
||||
user = from_user.key.urlsafe()
|
||||
for i, (target, orig_obj) in enumerate(sorted_targets):
|
||||
orig_obj = orig_obj.key.urlsafe() if orig_obj else ''
|
||||
user = g.user.key.urlsafe() if g.user else ''
|
||||
common.create_task(queue='send', obj=obj.key.urlsafe(),
|
||||
url=target.uri, protocol=target.protocol,
|
||||
orig_obj=orig_obj, user=user)
|
||||
|
@ -1210,8 +1210,8 @@ def send_task():
|
|||
orig_obj (url-safe google.cloud.ndb.key.Key): optional "original object"
|
||||
:class:`models.Object` that this object refers to, eg replies to or
|
||||
reposts or likes
|
||||
user (url-safe google.cloud.ndb.key.Key): :class:`models.User` this
|
||||
activity is on behalf of. This user will be loaded into ``g.user``
|
||||
user (url-safe google.cloud.ndb.key.Key): :class:`models.User` (actor)
|
||||
this activity is from
|
||||
"""
|
||||
form = request.form.to_dict()
|
||||
logger.info(f'Params: {list(form.items())}')
|
||||
|
@ -1229,15 +1229,16 @@ def send_task():
|
|||
logger.info(f"{url} not in {obj.key.id()} undelivered or failed, giving up")
|
||||
return r'¯\_(ツ)_/¯', 204
|
||||
|
||||
user = None
|
||||
if user_key := form.get('user'):
|
||||
g.user = ndb.Key(urlsafe=user_key).get()
|
||||
g.user = user = ndb.Key(urlsafe=user_key).get()
|
||||
orig_obj = (ndb.Key(urlsafe=form['orig_obj']).get()
|
||||
if form.get('orig_obj') else None)
|
||||
|
||||
# send
|
||||
sent = None
|
||||
try:
|
||||
sent = PROTOCOLS[protocol].send(obj, url, orig_obj=orig_obj)
|
||||
sent = PROTOCOLS[protocol].send(obj, url, from_user=user, orig_obj=orig_obj)
|
||||
except BaseException as e:
|
||||
code, body = util.interpret_http_exception(e)
|
||||
if not code and not body:
|
||||
|
|
|
@ -101,7 +101,7 @@ def redir(to):
|
|||
return f'Object not found: {to}', 404
|
||||
|
||||
g.user = Web.get_or_create(util.domain_from_link(to), direct=False, obj=obj)
|
||||
ret = ActivityPub.convert(obj)
|
||||
ret = ActivityPub.convert(obj, from_user=g.user)
|
||||
logger.info(f'Returning: {json_dumps(ret, indent=2)}')
|
||||
return ret, {
|
||||
'Content-Type': accept_type,
|
||||
|
|
|
@ -1105,7 +1105,9 @@ class ActivityPubTest(TestCase):
|
|||
self.assertEqual(202, got.status_code)
|
||||
self.assertEqual('inactive', follower.key.get().status)
|
||||
|
||||
def test_inbox_unsupported_type(self, *_):
|
||||
def test_inbox_unsupported_type(self, mock_head, mock_get, mock_post):
|
||||
mock_get.return_value = self.as2_resp(ACTOR)
|
||||
|
||||
got = self.post('/user.com/inbox', json={
|
||||
'@context': ['https://www.w3.org/ns/activitystreams'],
|
||||
'id': 'https://xoxo.zone/users/aaronpk#follows/40',
|
||||
|
@ -1790,7 +1792,6 @@ class ActivityPubUtilsTest(TestCase):
|
|||
}))
|
||||
|
||||
def test_postprocess_as2_actor_url_attachments(self):
|
||||
g.user = self.user
|
||||
got = postprocess_as2_actor(as2.from_as1({
|
||||
'objectType': 'person',
|
||||
'urls': [
|
||||
|
@ -1808,7 +1809,7 @@ class ActivityPubUtilsTest(TestCase):
|
|||
'displayName': 'two title',
|
||||
},
|
||||
]
|
||||
}))
|
||||
}), user=self.user)
|
||||
|
||||
self.assert_equals([{
|
||||
'type': 'PropertyValue',
|
||||
|
@ -1843,7 +1844,7 @@ class ActivityPubUtilsTest(TestCase):
|
|||
'name': 'nick',
|
||||
'value': '<a rel="me" href="https://user.com/about-me"><span class="invisible">https://</span>user.com/about-me</a>',
|
||||
}],
|
||||
})['preferredUsername'])
|
||||
}, user=self.user)['preferredUsername'])
|
||||
|
||||
def test_postprocess_as2_mentions_into_cc(self):
|
||||
obj = copy.deepcopy(MENTION_OBJECT)
|
||||
|
@ -1886,9 +1887,8 @@ class ActivityPubUtilsTest(TestCase):
|
|||
second['auth'].header_signer.sign(second['headers'], method='GET', path='/'))
|
||||
|
||||
@patch('requests.post', return_value=requests_response(status=200))
|
||||
def test_signed_post_g_user_is_activitypub_so_use_default_user(self, mock_post):
|
||||
g.user = ActivityPub(id='http://feddy')
|
||||
activitypub.signed_post('https://url')
|
||||
def test_signed_post_from_user_is_activitypub_so_use_default_user(self, mock_post):
|
||||
activitypub.signed_post('https://url', from_user=ActivityPub(id='http://fed'))
|
||||
|
||||
self.assertEqual(1, len(mock_post.call_args_list))
|
||||
args, kwargs = mock_post.call_args_list[0]
|
||||
|
@ -1903,8 +1903,7 @@ class ActivityPubUtilsTest(TestCase):
|
|||
allow_redirects=False),
|
||||
]
|
||||
|
||||
g.user = self.user
|
||||
resp = activitypub.signed_post('https://first')
|
||||
resp = activitypub.signed_post('https://first', from_user=self.user)
|
||||
mock_post.assert_called_once()
|
||||
self.assertEqual(302, resp.status_code)
|
||||
|
||||
|
@ -2177,8 +2176,7 @@ class ActivityPubUtilsTest(TestCase):
|
|||
'object': 'fake:post',
|
||||
'actor': 'fake:user',
|
||||
})
|
||||
g.user = self.user
|
||||
self.assertTrue(ActivityPub.send(like, 'https://inbox'))
|
||||
self.assertTrue(ActivityPub.send(like, 'https://inbox', from_user=self.user))
|
||||
|
||||
self.assertEqual(1, len(mock_post.call_args_list))
|
||||
args, kwargs = mock_post.call_args_list[0]
|
||||
|
|
|
@ -300,7 +300,11 @@ A ☕ reply
|
|||
|
||||
resp = self.client.get(f'/convert/ap/http://nope.com/post',
|
||||
base_url='https://web.brid.gy/')
|
||||
self.assertEqual(400, resp.status_code)
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assert_equals({
|
||||
**COMMENT_AS2,
|
||||
'attributedTo': 'https://fed.brid.gy/nope.com',
|
||||
}, resp.json, ignore=['to'])
|
||||
|
||||
@patch('requests.get')
|
||||
def test_web_to_activitypub_url_decode(self, mock_get):
|
||||
|
|
|
@ -1132,7 +1132,7 @@ class ProtocolReceiveTest(TestCase):
|
|||
}
|
||||
|
||||
sent = []
|
||||
def send(obj, url, orig_obj=None):
|
||||
def send(obj, url, from_user=None, orig_obj=None):
|
||||
self.assertEqual(create_as1, obj.as1)
|
||||
if not sent:
|
||||
self.assertEqual('target:1', url)
|
||||
|
|
|
@ -480,6 +480,13 @@ class WebTest(TestCase):
|
|||
self.assertEqual('☃.net', user.key.id())
|
||||
self.assert_entities_equal(user, Web.get_by_id('☃.net'))
|
||||
|
||||
def test_get_or_create_home_page_url(self, mock_get, mock_post):
|
||||
mock_get.return_value = requests_response('')
|
||||
|
||||
user = Web.get_or_create('https://foo.com/')
|
||||
self.assertEqual('foo.com', user.key.id())
|
||||
self.assert_entities_equal(user, Web.get_by_id('foo.com'))
|
||||
|
||||
def test_get_or_create_scripts_leading_trailing_dots(self, mock_get, mock_post):
|
||||
mock_get.return_value = requests_response('')
|
||||
|
||||
|
@ -931,6 +938,7 @@ class WebTest(TestCase):
|
|||
Object(id='https://user.com/', mf2=ACTOR_MF2).put()
|
||||
mock_get.side_effect = [
|
||||
LIKE,
|
||||
ACTOR_HTML_RESP,
|
||||
]
|
||||
|
||||
got = self.post('/queue/webmention', data={
|
||||
|
@ -1840,8 +1848,7 @@ http://this/404s
|
|||
# preferredUsername stays y.z despite user's username. since Mastodon
|
||||
# queries Webfinger for preferredUsername@fed.brid.gy
|
||||
# https://github.com/snarfed/bridgy-fed/issues/77#issuecomment-949955109
|
||||
g.user = self.user
|
||||
postprocessed = ActivityPub.convert(self.user.obj)
|
||||
postprocessed = ActivityPub.convert(self.user.obj, from_user=self.user)
|
||||
self.assertEqual('user.com', postprocessed['preferredUsername'])
|
||||
|
||||
def test_web_url(self, _, __):
|
||||
|
@ -2284,7 +2291,7 @@ class WebUtilTest(TestCase):
|
|||
</span>
|
||||
</body>
|
||||
</html>
|
||||
""", Web.convert(obj), ignore_blanks=True)
|
||||
""", Web.convert(obj, from_user=None), ignore_blanks=True)
|
||||
|
||||
def test_convert_translates_ids(self, *_):
|
||||
self.store_object(id='http://fed/post', source_protocol='activitypub')
|
||||
|
@ -2312,7 +2319,7 @@ class WebUtilTest(TestCase):
|
|||
'displayName': 'Ms. Alice',
|
||||
},
|
||||
},
|
||||
})), ignore_blanks=True)
|
||||
}), from_user=None), ignore_blanks=True)
|
||||
|
||||
def test_target_for(self, _, __):
|
||||
self.assertIsNone(Web.target_for(Object(id='x', source_protocol='web')))
|
||||
|
|
|
@ -108,7 +108,7 @@ class Fake(User, protocol.Protocol):
|
|||
return url.startswith(f'{cls.LABEL}:blocklisted')
|
||||
|
||||
@classmethod
|
||||
def send(cls, obj, url, orig_obj=None):
|
||||
def send(cls, obj, url, from_user=None, orig_obj=None):
|
||||
logger.info(f'{cls.__name__}.send {url}')
|
||||
cls.sent.append((obj.key.id(), url))
|
||||
return True
|
||||
|
@ -126,8 +126,8 @@ class Fake(User, protocol.Protocol):
|
|||
return False
|
||||
|
||||
@classmethod
|
||||
def convert(cls, obj):
|
||||
logger.info(f'{cls.__name__}.convert {obj.key.id()}')
|
||||
def convert(cls, obj, from_user=None):
|
||||
logger.info(f'{cls.__name__}.convert {obj.key.id()} {from_user}')
|
||||
return cls.translate_ids(obj.as1)
|
||||
|
||||
@classmethod
|
||||
|
|
13
web.py
13
web.py
|
@ -94,7 +94,8 @@ class Web(User, Protocol):
|
|||
Normalizing currently consists of lower casing and removing leading and
|
||||
trailing dots.
|
||||
"""
|
||||
return super().get_or_create(id.lower().strip('.'), **kwargs)
|
||||
domain = cls.key_for(id).id().lower().strip('.')
|
||||
return super().get_or_create(domain, **kwargs)
|
||||
|
||||
@ndb.ComputedProperty
|
||||
def handle(self):
|
||||
|
@ -307,7 +308,7 @@ class Web(User, Protocol):
|
|||
return obj.key.id()
|
||||
|
||||
@classmethod
|
||||
def send(to_cls, obj, url, orig_obj=None, **kwargs):
|
||||
def send(to_cls, obj, url, from_user=None, orig_obj=None, **kwargs):
|
||||
"""Sends a webmention to a given target URL.
|
||||
|
||||
See :meth:`Protocol.send` for details.
|
||||
|
@ -440,11 +441,12 @@ class Web(User, Protocol):
|
|||
return True
|
||||
|
||||
@classmethod
|
||||
def convert(cls, obj):
|
||||
def convert(cls, obj, from_user=None):
|
||||
"""Converts a :class:`Object` to HTML.
|
||||
|
||||
Args:
|
||||
obj (models.Object)
|
||||
from_user (models.User): user (actor) this activity/object is from
|
||||
|
||||
Returns:
|
||||
str:
|
||||
|
@ -608,8 +610,7 @@ def webmention_task():
|
|||
|
||||
# if source is home page, update Web user and send an actor Update to
|
||||
# followers' instances
|
||||
if user and (user.key.id() == obj.key.id()
|
||||
or user.is_web_url(obj.key.id())):
|
||||
if user.key.id() == obj.key.id() or user.is_web_url(obj.key.id()):
|
||||
logger.info(f'Converted to AS1: {obj.type}: {json_dumps(obj.as1, indent=2)}')
|
||||
obj.put()
|
||||
user.obj = obj
|
||||
|
@ -631,7 +632,7 @@ def webmention_task():
|
|||
})
|
||||
|
||||
try:
|
||||
return Web.receive(obj, authed_as=user)
|
||||
return Web.receive(obj, authed_as=f'https://{domain}/')
|
||||
except ValueError as e:
|
||||
logger.warning(e, exc_info=True)
|
||||
error(e, status=304)
|
||||
|
|
Ładowanie…
Reference in New Issue