2017-08-15 14:39:22 +00:00
|
|
|
"""Handles inbound webmentions.
|
2017-08-26 22:20:54 +00:00
|
|
|
|
2017-09-02 03:49:00 +00:00
|
|
|
TODO tests:
|
|
|
|
* actor/attributedTo could be string URL
|
2017-08-15 14:39:22 +00:00
|
|
|
"""
|
|
|
|
import logging
|
2019-12-26 06:20:57 +00:00
|
|
|
import urllib.parse
|
2021-07-11 15:48:28 +00:00
|
|
|
from urllib.parse import urlencode
|
2017-08-15 14:39:22 +00:00
|
|
|
|
2017-08-23 15:14:51 +00:00
|
|
|
import feedparser
|
2023-01-24 02:57:49 +00:00
|
|
|
from flask import redirect, request
|
2021-07-11 15:48:28 +00:00
|
|
|
from flask.views import View
|
2019-12-26 06:20:57 +00:00
|
|
|
from google.cloud.ndb import Key
|
2023-02-14 15:40:37 +00:00
|
|
|
from granary import as1, as2, microformats2
|
2017-08-15 14:39:22 +00:00
|
|
|
import mf2util
|
2021-07-18 04:22:13 +00:00
|
|
|
from oauth_dropins.webutil import flask_util, util
|
2023-01-05 04:48:39 +00:00
|
|
|
from oauth_dropins.webutil.appengine_config import tasks_client
|
|
|
|
from oauth_dropins.webutil.appengine_info import APP_ID
|
2023-01-24 02:57:49 +00:00
|
|
|
from oauth_dropins.webutil.flask_util import error, flash
|
2019-12-25 07:26:58 +00:00
|
|
|
from oauth_dropins.webutil.util import json_dumps, json_loads
|
2017-08-15 14:42:29 +00:00
|
|
|
import requests
|
2023-01-24 02:57:49 +00:00
|
|
|
from werkzeug.exceptions import BadGateway, HTTPException
|
2017-08-15 14:39:22 +00:00
|
|
|
|
|
|
|
import activitypub
|
2021-07-11 15:48:28 +00:00
|
|
|
from app import app
|
2017-08-15 14:39:22 +00:00
|
|
|
import common
|
2023-02-01 21:19:41 +00:00
|
|
|
from models import Follower, Object, Target, User
|
2017-08-15 14:39:22 +00:00
|
|
|
|
2022-02-12 06:38:56 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2018-02-02 15:41:37 +00:00
|
|
|
SKIP_EMAIL_DOMAINS = frozenset(('localhost', 'snarfed.org'))
|
|
|
|
|
2023-01-05 04:48:39 +00:00
|
|
|
# https://cloud.google.com/appengine/docs/locations
|
|
|
|
TASKS_LOCATION = 'us-central1'
|
|
|
|
|
2017-08-15 14:39:22 +00:00
|
|
|
|
2021-07-11 15:48:28 +00:00
|
|
|
class Webmention(View):
|
2023-01-05 03:22:11 +00:00
|
|
|
"""Handles inbound webmention, converts to ActivityPub."""
|
2023-01-05 04:48:39 +00:00
|
|
|
IS_TASK = False
|
|
|
|
|
2018-11-13 15:26:50 +00:00
|
|
|
source_url = None # string
|
|
|
|
source_domain = None # string
|
|
|
|
source_mf2 = None # parsed mf2 dict
|
2023-01-24 00:09:25 +00:00
|
|
|
source_as1 = None # AS1 dict
|
|
|
|
source_as2 = None # AS2 dict
|
2022-11-24 16:20:04 +00:00
|
|
|
user = None # User
|
2017-08-15 14:39:22 +00:00
|
|
|
|
2021-07-11 15:48:28 +00:00
|
|
|
def dispatch_request(self):
|
2022-02-12 06:38:56 +00:00
|
|
|
logger.info(f'Params: {list(request.form.items())}')
|
2017-08-15 14:39:22 +00:00
|
|
|
|
2023-01-24 00:09:25 +00:00
|
|
|
source = flask_util.get_required_param('source').strip()
|
2023-01-24 02:57:49 +00:00
|
|
|
self.source_domain = util.domain_from_link(source, minimize=False)
|
|
|
|
logger.info(f'webmention from {self.source_domain}')
|
2023-01-24 00:09:25 +00:00
|
|
|
|
2023-02-10 18:53:39 +00:00
|
|
|
self.user = User.get_by_id(self.source_domain)
|
|
|
|
if not self.user:
|
|
|
|
error(f'No user found for domain {self.source_domain}')
|
|
|
|
|
2023-01-24 00:09:25 +00:00
|
|
|
# if source is home page, send an actor Update to followers' instances
|
2023-02-12 20:03:27 +00:00
|
|
|
if self.user.is_homepage(source):
|
2023-01-24 00:09:25 +00:00
|
|
|
self.source_url = source
|
2023-02-08 02:25:24 +00:00
|
|
|
self.source_mf2, actor_as1, actor_as2 = common.actor(self.user)
|
2023-01-29 04:49:20 +00:00
|
|
|
id = common.host_url(f'{source}#update-{util.now().isoformat()}')
|
2023-01-24 00:09:25 +00:00
|
|
|
self.source_as1 = {
|
|
|
|
'objectType': 'activity',
|
|
|
|
'verb': 'update',
|
|
|
|
'id': id,
|
2023-02-07 20:43:36 +00:00
|
|
|
'url': id,
|
2023-01-24 00:09:25 +00:00
|
|
|
'object': actor_as1,
|
|
|
|
}
|
|
|
|
self.source_as2 = common.postprocess_as2({
|
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
|
|
'type': 'Update',
|
2023-02-07 20:43:36 +00:00
|
|
|
'id': id,
|
|
|
|
'url': id,
|
2023-01-24 00:09:25 +00:00
|
|
|
'object': actor_as2,
|
|
|
|
}, user=self.user)
|
|
|
|
return self.try_activitypub() or 'No ActivityPub targets'
|
|
|
|
|
2018-11-13 15:26:50 +00:00
|
|
|
# fetch source page
|
2022-11-24 16:20:04 +00:00
|
|
|
try:
|
2022-12-21 05:52:26 +00:00
|
|
|
source_resp = util.requests_get(source, gateway=True)
|
2022-11-24 16:20:04 +00:00
|
|
|
except ValueError as e:
|
|
|
|
error(f'Bad source URL: {source}: {e}')
|
2018-11-13 15:26:50 +00:00
|
|
|
self.source_url = source_resp.url or source
|
2019-12-26 06:20:57 +00:00
|
|
|
self.source_domain = urllib.parse.urlparse(self.source_url).netloc.split(':')[0]
|
2022-11-18 10:18:35 +00:00
|
|
|
fragment = urllib.parse.urlparse(self.source_url).fragment
|
|
|
|
self.source_mf2 = util.parse_mf2(source_resp, id=fragment)
|
2019-10-04 04:08:26 +00:00
|
|
|
|
2023-01-24 00:09:25 +00:00
|
|
|
if fragment and self.source_mf2 is None:
|
2023-01-24 02:57:49 +00:00
|
|
|
error(f'#{fragment} not found in {self.source_url}')
|
2022-11-18 23:28:34 +00:00
|
|
|
|
2022-02-12 06:38:56 +00:00
|
|
|
# logger.debug(f'Parsed mf2 for {source_resp.url} : {json_dumps(self.source_mf2 indent=2)}')
|
2018-11-13 15:26:50 +00:00
|
|
|
|
2023-01-24 00:09:25 +00:00
|
|
|
# check for backlink for webmention spec and to confirm source's intent
|
|
|
|
# to federate
|
2022-11-24 17:38:30 +00:00
|
|
|
for domain in common.DOMAINS:
|
|
|
|
if domain in source_resp.text:
|
|
|
|
break
|
|
|
|
else:
|
2023-01-05 23:03:21 +00:00
|
|
|
error(f"Couldn't find link to {common.host_url().rstrip('/')}")
|
2018-11-27 15:27:00 +00:00
|
|
|
|
2018-11-13 15:26:50 +00:00
|
|
|
# convert source page to ActivityStreams
|
|
|
|
entry = mf2util.find_first_entry(self.source_mf2, ['h-entry'])
|
|
|
|
if not entry:
|
2021-11-01 23:14:36 +00:00
|
|
|
error(f'No microformats2 found on {self.source_url}')
|
2018-11-13 15:26:50 +00:00
|
|
|
|
2022-12-23 23:38:38 +00:00
|
|
|
logger.info(f'First entry (id={fragment}): {json_dumps(entry, indent=2)}')
|
2018-11-13 15:26:50 +00:00
|
|
|
# make sure it has url, since we use that for AS2 id, which is required
|
|
|
|
# for ActivityPub.
|
|
|
|
props = entry.setdefault('properties', {})
|
|
|
|
if not props.get('url'):
|
|
|
|
props['url'] = [self.source_url]
|
|
|
|
|
2023-01-24 00:09:25 +00:00
|
|
|
self.source_as1 = microformats2.json_to_object(entry, fetch_mf2=True)
|
2022-11-29 01:48:33 +00:00
|
|
|
type_label = ' '.join((
|
2023-01-24 00:09:25 +00:00
|
|
|
self.source_as1.get('verb', ''),
|
|
|
|
self.source_as1.get('objectType', ''),
|
|
|
|
util.get_first(self.source_as1, 'object', {}).get('objectType', ''),
|
2022-11-29 01:48:33 +00:00
|
|
|
))
|
2023-01-24 00:09:25 +00:00
|
|
|
logger.info(f'Converted webmention to AS1: {type_label}: {json_dumps(self.source_as1, indent=2)}')
|
2018-11-13 15:26:50 +00:00
|
|
|
|
2023-01-05 03:22:11 +00:00
|
|
|
ret = self.try_activitypub()
|
2023-01-05 04:48:39 +00:00
|
|
|
return ret or 'No ActivityPub targets'
|
2018-11-13 15:26:50 +00:00
|
|
|
|
2017-10-26 14:30:52 +00:00
|
|
|
def try_activitypub(self):
|
2020-11-13 17:50:14 +00:00
|
|
|
"""Attempts ActivityPub delivery.
|
|
|
|
|
2021-07-11 15:48:28 +00:00
|
|
|
Returns Flask response (string body or tuple) if we succeeded or failed,
|
|
|
|
None if ActivityPub was not available.
|
2020-11-13 17:50:14 +00:00
|
|
|
"""
|
2023-01-29 17:45:03 +00:00
|
|
|
inboxes_to_targets = self._activitypub_targets()
|
|
|
|
if not inboxes_to_targets:
|
2020-11-13 17:50:14 +00:00
|
|
|
return None
|
2017-10-26 14:30:52 +00:00
|
|
|
|
2018-12-11 16:00:38 +00:00
|
|
|
error = None
|
2020-06-06 15:39:44 +00:00
|
|
|
last_success = None
|
2023-01-25 00:47:23 +00:00
|
|
|
log_data = True
|
2017-08-15 14:39:22 +00:00
|
|
|
|
2023-01-31 06:02:23 +00:00
|
|
|
type = as1.object_type(self.source_as1)
|
2023-02-07 20:23:08 +00:00
|
|
|
obj_id = self.source_as1.get('id') or self.source_url
|
|
|
|
obj = Object.get_by_id(obj_id)
|
2023-01-31 06:02:23 +00:00
|
|
|
changed = False
|
|
|
|
|
2023-01-29 17:45:03 +00:00
|
|
|
if obj:
|
2023-01-31 06:02:23 +00:00
|
|
|
logging.info(f'Resuming existing {obj}')
|
2023-02-01 20:22:04 +00:00
|
|
|
obj.failed = []
|
2023-02-01 21:19:41 +00:00
|
|
|
seen = [t.uri for t in obj.delivered + obj.undelivered + obj.failed]
|
2023-01-29 17:45:03 +00:00
|
|
|
new_inboxes = [i for i in inboxes_to_targets.keys() if i not in seen]
|
|
|
|
if new_inboxes:
|
|
|
|
logging.info(f'Adding new inboxes: {new_inboxes}')
|
2023-02-01 21:19:41 +00:00
|
|
|
obj.undelivered += [Target(uri=uri, protocol='activitypub')
|
|
|
|
for uri in new_inboxes]
|
2023-01-31 06:02:23 +00:00
|
|
|
if type in ('note', 'article', 'comment'):
|
|
|
|
changed = as1.activity_changed(json_loads(obj.as1), self.source_as1)
|
|
|
|
if changed:
|
2023-02-01 20:22:04 +00:00
|
|
|
obj.undelivered += obj.delivered
|
|
|
|
obj.delivered = []
|
|
|
|
logger.info(f'Content has changed from last time at {obj.updated}! Redelivering to all inboxes: {obj.undelivered}')
|
2023-01-31 06:02:23 +00:00
|
|
|
|
2023-01-29 17:45:03 +00:00
|
|
|
else:
|
2023-02-07 20:23:08 +00:00
|
|
|
obj = Object(id=obj_id, delivered=[], failed=[],
|
2023-02-01 21:19:41 +00:00
|
|
|
undelivered=[Target(uri=uri, protocol='activitypub')
|
2023-02-03 03:51:03 +00:00
|
|
|
for uri in inboxes_to_targets.keys()],
|
|
|
|
status='in progress')
|
2023-02-07 16:24:36 +00:00
|
|
|
logging.info(f'Storing new Object {self.source_url}')
|
2023-01-31 06:02:23 +00:00
|
|
|
|
|
|
|
obj.domains = [self.source_domain]
|
2023-02-01 05:00:07 +00:00
|
|
|
obj.source_protocol = 'webmention'
|
2023-01-31 06:02:23 +00:00
|
|
|
obj.mf2 = json_dumps(self.source_mf2)
|
|
|
|
obj.as1 = json_dumps(self.source_as1)
|
2023-02-01 05:00:07 +00:00
|
|
|
obj.labels = ['user']
|
2023-02-01 20:22:04 +00:00
|
|
|
if self.source_as1.get('objectType') == 'activity':
|
|
|
|
obj.labels.append('activity')
|
2023-01-29 22:13:58 +00:00
|
|
|
obj.put()
|
|
|
|
|
2023-01-29 04:49:20 +00:00
|
|
|
# TODO: collect by inbox, add 'to' fields, de-dupe inboxes and recipients
|
2023-01-29 17:45:03 +00:00
|
|
|
#
|
2023-02-01 20:22:04 +00:00
|
|
|
# make copy of undelivered because we modify it below
|
2023-02-01 21:19:41 +00:00
|
|
|
logger.info(f'Delivering to inboxes: {sorted(t.uri for t in obj.undelivered)}')
|
|
|
|
for target in list(obj.undelivered):
|
|
|
|
inbox = target.uri
|
2023-01-29 17:45:03 +00:00
|
|
|
if inbox in inboxes_to_targets:
|
|
|
|
target_as2 = inboxes_to_targets[inbox]
|
|
|
|
else:
|
|
|
|
logging.warning(f'Missing target_as2 for inbox {inbox}!')
|
|
|
|
target_as2 = None
|
|
|
|
|
2023-01-24 00:09:25 +00:00
|
|
|
if not self.source_as2:
|
|
|
|
self.source_as2 = common.postprocess_as2(
|
2023-01-29 04:49:20 +00:00
|
|
|
as2.from_as1(self.source_as1), target=target_as2, user=self.user)
|
2023-01-24 00:09:25 +00:00
|
|
|
if not self.source_as2.get('actor'):
|
|
|
|
self.source_as2['actor'] = common.host_url(self.source_domain)
|
2023-01-31 06:02:23 +00:00
|
|
|
if changed:
|
2023-01-29 04:49:20 +00:00
|
|
|
self.source_as2['type'] = 'Update'
|
2023-01-24 00:09:25 +00:00
|
|
|
|
2023-01-24 04:53:34 +00:00
|
|
|
if self.source_as2.get('type') == 'Update':
|
|
|
|
# Mastodon requires the updated field for Updates, so
|
|
|
|
# generate it if it's not already there.
|
|
|
|
# https://docs.joinmastodon.org/spec/activitypub/#supported-activities-for-statuses
|
|
|
|
# https://socialhub.activitypub.rocks/t/what-could-be-the-reason-that-my-update-activity-does-not-work/2893/4
|
|
|
|
# https://github.com/mastodon/documentation/pull/1150
|
|
|
|
self.source_as2.get('object', {}).setdefault(
|
|
|
|
'updated', util.now().isoformat())
|
2018-11-13 15:26:50 +00:00
|
|
|
|
2023-01-24 00:09:25 +00:00
|
|
|
if self.source_as2.get('type') == 'Follow':
|
2022-11-24 18:16:35 +00:00
|
|
|
# prefer AS2 id or url, if available
|
|
|
|
# https://github.com/snarfed/bridgy-fed/issues/307
|
2023-01-29 04:49:20 +00:00
|
|
|
dest = ((target_as2.get('id') or util.get_first(target_as2, 'url'))
|
|
|
|
if target_as2 else util.get_url(self.source_as1, 'object'))
|
2022-11-24 18:16:35 +00:00
|
|
|
Follower.get_or_create(dest=dest, src=self.source_domain,
|
2023-01-24 00:09:25 +00:00
|
|
|
last_follow=json_dumps(self.source_as2))
|
2022-11-24 18:16:35 +00:00
|
|
|
|
2018-11-13 15:26:50 +00:00
|
|
|
try:
|
2023-02-07 03:23:25 +00:00
|
|
|
last = common.signed_post(inbox, user=self.user, data=self.source_as2,
|
|
|
|
log_data=log_data)
|
2023-02-01 21:19:41 +00:00
|
|
|
obj.delivered.append(target)
|
2020-06-06 15:39:44 +00:00
|
|
|
last_success = last
|
2018-12-11 16:00:38 +00:00
|
|
|
except BaseException as e:
|
2023-02-01 21:19:41 +00:00
|
|
|
code, body = util.interpret_http_exception(e)
|
|
|
|
if not code and not body:
|
|
|
|
raise
|
|
|
|
obj.failed.append(target)
|
2018-12-11 16:00:38 +00:00
|
|
|
error = e
|
2023-01-25 00:47:23 +00:00
|
|
|
finally:
|
|
|
|
log_data = False
|
2018-11-13 15:26:50 +00:00
|
|
|
|
2023-02-01 21:19:41 +00:00
|
|
|
obj.undelivered.remove(target)
|
2023-01-29 17:45:03 +00:00
|
|
|
obj.put()
|
|
|
|
|
2023-02-01 21:19:41 +00:00
|
|
|
obj.status = ('complete' if obj.delivered
|
|
|
|
else 'failed' if obj.failed
|
|
|
|
else 'ignored')
|
2023-01-29 04:49:20 +00:00
|
|
|
obj.put()
|
2018-11-13 15:26:50 +00:00
|
|
|
|
|
|
|
# Pass the AP response status code and body through as our response
|
2020-06-06 15:39:44 +00:00
|
|
|
if last_success:
|
2021-07-20 22:55:16 +00:00
|
|
|
return last_success.text or 'Sent!', last_success.status_code
|
2021-08-16 18:47:31 +00:00
|
|
|
elif isinstance(error, BadGateway):
|
|
|
|
raise error
|
|
|
|
elif isinstance(error, requests.HTTPError):
|
2021-07-11 15:48:28 +00:00
|
|
|
return str(error), error.status_code
|
2018-12-11 16:00:38 +00:00
|
|
|
else:
|
2021-07-11 15:48:28 +00:00
|
|
|
return str(error)
|
2018-11-13 15:26:50 +00:00
|
|
|
|
2023-01-05 04:48:39 +00:00
|
|
|
def _activitypub_targets(self):
|
2018-11-19 00:58:52 +00:00
|
|
|
"""
|
2023-01-29 17:45:03 +00:00
|
|
|
Returns: dict of {str inbox URL: dict target AS2 object}
|
2018-11-19 00:58:52 +00:00
|
|
|
"""
|
2023-01-05 04:48:39 +00:00
|
|
|
# if there's in-reply-to, like-of, or repost-of, they're the targets.
|
|
|
|
# otherwise, it's all followers' inboxes.
|
2023-01-24 00:09:25 +00:00
|
|
|
targets = util.get_urls(self.source_as1, 'inReplyTo')
|
2020-06-06 15:39:44 +00:00
|
|
|
if targets:
|
2022-11-14 15:07:33 +00:00
|
|
|
logger.info(f'targets from inReplyTo: {targets}')
|
2023-01-24 00:09:25 +00:00
|
|
|
elif self.source_as1.get('verb') in as1.VERBS_WITH_OBJECT:
|
|
|
|
targets = util.get_urls(self.source_as1, 'object')
|
2022-11-14 15:07:33 +00:00
|
|
|
logger.info(f'targets from object: {targets}')
|
2017-08-15 14:39:22 +00:00
|
|
|
|
2020-06-06 15:39:44 +00:00
|
|
|
if not targets:
|
2023-01-05 04:48:39 +00:00
|
|
|
# interpret this as a Create or Update, deliver it to followers. use
|
|
|
|
# task queue since we send to each inbox in serial, which can take a
|
|
|
|
# long time with many followers/instances.
|
|
|
|
if not self.IS_TASK:
|
|
|
|
queue_path= tasks_client.queue_path(APP_ID, TASKS_LOCATION, 'webmention')
|
|
|
|
tasks_client.create_task(
|
|
|
|
parent=queue_path,
|
|
|
|
task={
|
|
|
|
'app_engine_http_request': {
|
|
|
|
'http_method': 'POST',
|
|
|
|
'relative_uri': '/_ah/queue/webmention',
|
|
|
|
'body': urlencode({'source': self.source_url}).encode(),
|
|
|
|
# https://googleapis.dev/python/cloudtasks/latest/gapic/v2/types.html#google.cloud.tasks_v2.types.AppEngineHttpRequest.headers
|
|
|
|
'headers': {'Content-Type': 'application/x-www-form-urlencoded'},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
# not actually an error
|
2023-01-24 05:13:05 +00:00
|
|
|
msg = ("Updating profile on followers' instances..."
|
2023-02-12 20:03:27 +00:00
|
|
|
if self.user.is_homepage(self.source_url)
|
2023-01-24 05:13:05 +00:00
|
|
|
else 'Delivering to followers...')
|
2023-01-24 02:57:49 +00:00
|
|
|
error(msg, status=202)
|
2023-01-05 04:48:39 +00:00
|
|
|
|
2021-02-24 21:41:46 +00:00
|
|
|
inboxes = set()
|
2018-11-20 16:37:57 +00:00
|
|
|
for follower in Follower.query().filter(
|
2018-11-13 15:26:50 +00:00
|
|
|
Follower.key > Key('Follower', self.source_domain + ' '),
|
2018-11-20 16:37:57 +00:00
|
|
|
Follower.key < Key('Follower', self.source_domain + chr(ord(' ') + 1))):
|
2019-08-01 14:36:34 +00:00
|
|
|
if follower.status != 'inactive' and follower.last_follow:
|
2019-12-25 07:26:58 +00:00
|
|
|
actor = json_loads(follower.last_follow).get('actor')
|
2018-11-20 17:47:01 +00:00
|
|
|
if actor and isinstance(actor, dict):
|
2021-02-24 21:41:46 +00:00
|
|
|
inboxes.add(actor.get('endpoints', {}).get('sharedInbox') or
|
2023-01-29 04:49:20 +00:00
|
|
|
actor.get('publicInbox') or
|
2021-02-24 21:41:46 +00:00
|
|
|
actor.get('inbox'))
|
2023-01-31 06:02:23 +00:00
|
|
|
logger.info('Delivering to followers')
|
2023-01-29 17:45:03 +00:00
|
|
|
return {inbox: None for inbox in inboxes}
|
2022-11-14 15:07:33 +00:00
|
|
|
|
|
|
|
targets = common.remove_blocklisted(targets)
|
|
|
|
if not targets:
|
|
|
|
error(f"Silo responses are not yet supported.")
|
2017-10-02 04:43:01 +00:00
|
|
|
|
2023-01-29 17:45:03 +00:00
|
|
|
inboxes_to_targets = {}
|
2020-06-06 15:39:44 +00:00
|
|
|
for target in targets:
|
2022-11-14 15:07:33 +00:00
|
|
|
# fetch target page as AS2 object
|
|
|
|
try:
|
2023-02-14 22:56:27 +00:00
|
|
|
target_obj = json_loads(
|
|
|
|
common.get_object(target, user=self.user).as2)
|
2022-11-14 15:07:33 +00:00
|
|
|
except (requests.HTTPError, BadGateway) as e:
|
2023-02-14 22:56:27 +00:00
|
|
|
resp = getattr(e, 'requests_response', None)
|
|
|
|
if resp and resp.ok:
|
|
|
|
if (common.content_type(resp) or '').startswith('text/html'):
|
2022-11-14 15:07:33 +00:00
|
|
|
continue # give up
|
|
|
|
raise
|
|
|
|
|
|
|
|
inbox_url = target_obj.get('inbox')
|
|
|
|
if not inbox_url:
|
|
|
|
# TODO: test actor/attributedTo and not, with/without inbox
|
|
|
|
actor = (util.get_first(target_obj, 'actor') or
|
|
|
|
util.get_first(target_obj, 'attributedTo'))
|
|
|
|
if isinstance(actor, dict):
|
|
|
|
inbox_url = actor.get('inbox')
|
2022-12-01 05:04:22 +00:00
|
|
|
actor = util.get_first(actor, 'url') or actor.get('id')
|
2022-11-14 15:07:33 +00:00
|
|
|
if not inbox_url and not actor:
|
|
|
|
error('Target object has no actor or attributedTo with URL or id.')
|
|
|
|
elif not isinstance(actor, str):
|
|
|
|
error(f'Target actor or attributedTo has unexpected url or id object: {actor}')
|
|
|
|
|
|
|
|
if not inbox_url:
|
|
|
|
# fetch actor as AS object
|
2023-02-14 22:56:27 +00:00
|
|
|
actor = json_loads(common.get_object(actor, user=self.user).as2)
|
2022-11-14 15:07:33 +00:00
|
|
|
inbox_url = actor.get('inbox')
|
|
|
|
|
|
|
|
if not inbox_url:
|
2023-01-05 03:22:11 +00:00
|
|
|
# TODO: probably need a way to surface errors like this
|
|
|
|
logging.error('Target actor has no inbox')
|
2022-11-14 15:07:33 +00:00
|
|
|
continue
|
|
|
|
|
2023-02-14 22:56:27 +00:00
|
|
|
inbox_url = urllib.parse.urljoin(target, inbox_url)
|
2023-01-29 17:45:03 +00:00
|
|
|
inboxes_to_targets[inbox_url] = target_obj
|
2022-11-14 15:07:33 +00:00
|
|
|
|
2023-01-29 17:45:03 +00:00
|
|
|
logger.info(f"Delivering to targets' inboxes: {inboxes_to_targets.keys()}")
|
|
|
|
return inboxes_to_targets
|
2017-08-23 15:14:51 +00:00
|
|
|
|
2017-08-15 14:39:22 +00:00
|
|
|
|
2023-01-05 04:48:39 +00:00
|
|
|
class WebmentionTask(Webmention):
|
2023-01-24 02:57:49 +00:00
|
|
|
"""Handler that runs tasks, not external HTTP requests."""
|
2023-01-05 04:48:39 +00:00
|
|
|
IS_TASK = True
|
|
|
|
|
|
|
|
|
2023-01-24 02:57:49 +00:00
|
|
|
class WebmentionInteractive(Webmention):
|
|
|
|
"""Handler that runs interactive webmention-based requests from the web UI.
|
|
|
|
|
|
|
|
...eg the update profile button on user pages.
|
|
|
|
"""
|
|
|
|
def dispatch_request(self):
|
|
|
|
try:
|
|
|
|
super().dispatch_request()
|
|
|
|
flash('OK')
|
|
|
|
except HTTPException as e:
|
|
|
|
flash(util.linkify(str(e.description), pretty=True))
|
|
|
|
return redirect(f'/user/{self.source_domain}', code=302)
|
|
|
|
|
|
|
|
|
2021-07-11 15:48:28 +00:00
|
|
|
app.add_url_rule('/webmention', view_func=Webmention.as_view('webmention'),
|
|
|
|
methods=['POST'])
|
2023-01-24 02:57:49 +00:00
|
|
|
app.add_url_rule('/webmention-interactive',
|
|
|
|
view_func=WebmentionInteractive.as_view('webmention-interactive'),
|
|
|
|
methods=['POST'])
|
2023-01-05 04:48:39 +00:00
|
|
|
app.add_url_rule('/_ah/queue/webmention',
|
|
|
|
view_func=WebmentionTask.as_view('webmention-task'),
|
|
|
|
methods=['POST'])
|