Exemple #1
0
    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))
Exemple #3
0
    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))
Exemple #4
0
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 ''
Exemple #5
0
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'))