def _test_inbox_mention(self, as2, mock_head, mock_get, mock_post): mock_head.return_value = requests_response(url='http://target') mock_get.return_value = requests_response( '<html><head><link rel="webmention" href="/webmention"></html>') mock_post.return_value = requests_response() got = app.get_response('/foo.com/inbox', method='POST', body=json.dumps(as2)) self.assertEquals(200, got.status_int, got.body) mock_get.assert_called_once_with('http://target/', headers=common.HEADERS, verify=False) expected_headers = copy.deepcopy(common.HEADERS) expected_headers['Accept'] = '*/*' mock_post.assert_called_once_with( 'http://target/webmention', data={ 'source': 'http://localhost/render?source=http%3A%2F%2Fthis%2Fmention&target=http%3A%2F%2Ftarget%2F', 'target': 'http://target/', }, allow_redirects=False, headers=expected_headers, verify=False) resp = Response.get_by_id('http://this/mention http://target/') self.assertEqual('in', resp.direction) self.assertEqual('activitypub', resp.protocol) self.assertEqual('complete', resp.status) self.assertEqual(common.redirect_unwrap(as2), json.loads(resp.source_as2))
def _test_inbox_mention(self, as2, mock_head, mock_get, mock_post): mock_head.return_value = requests_response(url='http://target') mock_get.return_value = requests_response( '<html><head><link rel="webmention" href="/webmention"></html>') mock_post.return_value = requests_response() with self.client: got = self.client.post('/foo.com/inbox', json=as2) self.assertEqual(200, got.status_code, got.get_data(as_text=True)) mock_get.assert_called_once_with( 'http://target/', headers=common.HEADERS, timeout=15, stream=True) expected_headers = copy.deepcopy(common.HEADERS) expected_headers['Accept'] = '*/*' mock_post.assert_called_once_with( 'http://target/webmention', data={ 'source': 'http://localhost/render?source=http%3A%2F%2Fthis%2Fmention&target=http%3A%2F%2Ftarget%2F', 'target': 'http://target/', }, allow_redirects=False, timeout=15, stream=True, headers=expected_headers) resp = Response.get_by_id('http://this/mention http://target/') self.assertEqual('in', resp.direction) self.assertEqual('activitypub', resp.protocol) self.assertEqual('complete', resp.status) self.assertEqual(common.redirect_unwrap(as2), json_loads(resp.source_as2))
def post(self, domain): logging.info('Got: %s', self.request.body) # parse and validate AS2 activity try: activity = json.loads(self.request.body) assert activity except (TypeError, ValueError, AssertionError): common.error(self, "Couldn't parse body as JSON", exc_info=True) obj = activity.get('object') or {} if isinstance(obj, basestring): obj = {'id': obj} type = activity.get('type') if type == 'Accept': # eg in response to a Follow return # noop if type == 'Create': type = obj.get('type') elif type not in SUPPORTED_TYPES: common.error(self, 'Sorry, %s activities are not supported yet.' % type, status=501) # TODO: verify signature if there is one # fetch actor if necessary so we have name, profile photo, etc for elem in obj, activity: actor = elem.get('actor') if actor and isinstance(actor, basestring): elem['actor'] = common.get_as2(actor).json() activity_unwrapped = common.redirect_unwrap(activity) if type == 'Follow': self.accept_follow(activity, activity_unwrapped) return # send webmentions to each target as1 = as2.to_as1(activity) common.send_webmentions(self, as1, proxy=True, protocol='activitypub', source_as2=json.dumps(activity_unwrapped))
def inbox(domain): """Accepts POSTs to /[DOMAIN]/inbox and converts to outbound webmentions.""" body = request.get_data(as_text=True) logging.info(f'Got: {body}') # parse and validate AS2 activity try: activity = request.json assert activity except (TypeError, ValueError, AssertionError): error("Couldn't parse body as JSON", exc_info=True) obj = activity.get('object') or {} if isinstance(obj, str): obj = {'id': obj} type = activity.get('type') if type == 'Accept': # eg in response to a Follow return '' # noop if type == 'Create': type = obj.get('type') elif type not in SUPPORTED_TYPES: error('Sorry, %s activities are not supported yet.' % type, status=501) # TODO: verify signature if there is one if type == 'Undo' and obj.get('type') == 'Follow': # skip actor fetch below; we don't need it to undo a follow undo_follow(redirect_unwrap(activity)) return '' elif type == 'Delete': id = obj.get('id') # !!! temporarily disabled actually deleting Followers below because # mastodon.social sends Deletes for every Bridgy Fed account, all at # basically the same time, and we have many Follower objects, so we # have to do this table scan for each one, so the requests take a # long time and end up spawning extra App Engine instances that we # get billed for. and the Delete requests are almost never for # followers we have. TODO: revisit this and do it right. # if isinstance(id, str): # # assume this is an actor # # https://github.com/snarfed/bridgy-fed/issues/63 # for key in Follower.query().iter(keys_only=True): # if key.id().split(' ')[-1] == id: # key.delete() return '' # fetch actor if necessary so we have name, profile photo, etc for elem in obj, activity: actor = elem.get('actor') if actor and isinstance(actor, str): elem['actor'] = common.get_as2(actor).json() activity_unwrapped = redirect_unwrap(activity) if type == 'Follow': return accept_follow(activity, activity_unwrapped) # send webmentions to each target as1 = as2.to_as1(activity) common.send_webmentions(as1, proxy=True, protocol='activitypub', source_as2=json_dumps(activity_unwrapped)) return ''
def send_webmentions(handler, activity_wrapped, proxy=None, **response_props): """Sends webmentions for an incoming Salmon slap or ActivityPub inbox delivery. Args: handler: RequestHandler activity_wrapped: dict, AS1 activity response_props: passed through to the newly created Responses """ activity = common.redirect_unwrap(activity_wrapped) verb = activity.get('verb') if verb and verb not in SUPPORTED_VERBS: error(handler, '%s activities are not supported yet.' % verb) # extract source and targets source = activity.get('url') or activity.get('id') obj = activity.get('object') obj_url = util.get_url(obj) targets = util.get_list(activity, 'inReplyTo') if isinstance(obj, dict): if not source or verb in ('create', 'post', 'update'): source = obj_url or obj.get('id') targets.extend(util.get_list(obj, 'inReplyTo')) tags = util.get_list(activity_wrapped, 'tags') obj_wrapped = activity_wrapped.get('object') if isinstance(obj_wrapped, dict): tags.extend(util.get_list(obj_wrapped, 'tags')) for tag in tags: if tag.get('objectType') == 'mention': url = tag.get('url') if url and url.startswith(appengine_config.HOST_URL): targets.append(redirect_unwrap(url)) if verb in ('follow', 'like', 'share'): targets.append(obj_url) targets = util.dedupe_urls(util.get_url(t) for t in targets) if not source: error(handler, "Couldn't find original post URL") if not targets: error(handler, "Couldn't find any target URLs in inReplyTo, object, or mention tags") # send webmentions and store Responses errors = [] for target in targets: if util.domain_from_link(target) == util.domain_from_link(source): logging.info('Skipping same-domain webmention from %s to %s', source, target) continue response = Response(source=source, target=target, direction='in', **response_props) response.put() wm_source = (response.proxy_url() if verb in ('follow', 'like', 'share') or proxy else source) logging.info('Sending webmention from %s to %s', wm_source, target) wm = send.WebmentionSend(wm_source, target) if wm.send(headers=HEADERS): logging.info('Success: %s', wm.response) response.status = 'complete' else: logging.warning('Failed: %s', wm.error) errors.append(wm.error) response.status = 'error' response.put() if errors: msg = 'Errors:\n' + '\n'.join(json.dumps(e, indent=2) for e in errors) error(handler, msg, status=errors[0].get('http_status'))