pass user to Protocol.send/convert instead of using g.user

for #690
g.user
Ryan Barrett 2023-11-25 20:07:14 -08:00
rodzic face71c9eb
commit 1591dfb641
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
12 zmienionych plików z 113 dodań i 120 usunięć

Wyświetl plik

@ -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'),

Wyświetl plik

@ -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

Wyświetl plik

@ -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}

Wyświetl plik

@ -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()

Wyświetl plik

@ -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:

Wyświetl plik

@ -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,

Wyświetl plik

@ -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]

Wyświetl plik

@ -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):

Wyświetl plik

@ -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)

Wyświetl plik

@ -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')))

Wyświetl plik

@ -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
Wyświetl plik

@ -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)